Completed
Pull Request — master (#1727)
by Damian
24:34
created

SiteTree::getClassDropdown()   C

Complexity

Conditions 10
Paths 26

Size

Total Lines 50
Code Lines 27

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 50
rs 5.7647
cc 10
eloc 27
nc 26
nop 0

How to fix   Complexity   

Long Method

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

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

Commonly applied refactorings include:

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

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

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

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

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

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

Loading history...
385
                    foreach ($alternatives as $alternative) {
386
                        if ($alternative) {
387
                            $next = $alternative;
388
                        }
389
                    }
390
                }
391
392
                if (!$next) {
393
                    return null;
394
                }
395
            }
396
397
            $sitetree->destroy();
398
            $sitetree = $next;
399
        }
400
401
        return $sitetree;
402
    }
403
404
    /**
405
     * Return a subclass map of SiteTree that shouldn't be hidden through {@link SiteTree::$hide_ancestor}
406
     *
407
     * @return array
408
     */
409
    public static function page_type_classes()
410
    {
411
        $classes = ClassInfo::getValidSubClasses();
412
413
        $baseClassIndex = array_search(self::class, $classes);
414
        if ($baseClassIndex !== false) {
415
            unset($classes[$baseClassIndex]);
416
        }
417
418
        $kill_ancestors = array();
419
420
        // figure out if there are any classes we don't want to appear
421
        foreach ($classes as $class) {
422
            $instance = singleton($class);
423
424
            // do any of the progeny want to hide an ancestor?
425
            if ($ancestor_to_hide = $instance->stat('hide_ancestor')) {
426
                // note for killing later
427
                $kill_ancestors[] = $ancestor_to_hide;
428
            }
429
        }
430
431
        // If any of the descendents don't want any of the elders to show up, cruelly render the elders surplus to
432
        // requirements
433
        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...
434
            $kill_ancestors = array_unique($kill_ancestors);
435
            foreach ($kill_ancestors as $mark) {
436
                // unset from $classes
437
                $idx = array_search($mark, $classes, true);
438
                if ($idx !== false) {
439
                    unset($classes[$idx]);
440
                }
441
            }
442
        }
443
444
        return $classes;
445
    }
446
447
    /**
448
     * Replace a "[sitetree_link id=n]" shortcode with a link to the page with the corresponding ID.
449
     *
450
     * @param array      $arguments
451
     * @param string     $content
452
     * @param ShortcodeParser $parser
453
     * @return string
454
     */
455
    public static function link_shortcode_handler($arguments, $content = null, $parser = null)
456
    {
457
        if (!isset($arguments['id']) || !is_numeric($arguments['id'])) {
458
            return null;
459
        }
460
461
        /** @var SiteTree $page */
462
        if (!($page = DataObject::get_by_id(self::class, $arguments['id']))         // Get the current page by ID.
463
            && !($page = Versioned::get_latest_version(self::class, $arguments['id'])) // Attempt link to old version.
464
        ) {
465
             return null; // There were no suitable matches at all.
466
        }
467
468
        /** @var SiteTree $page */
469
        $link = Convert::raw2att($page->Link());
470
471
        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...
472
            return sprintf('<a href="%s">%s</a>', $link, $parser->parse($content));
0 ignored issues
show
Bug introduced by
It seems like $parser is not always an object, but can also be of type null. Maybe add an additional type check?

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

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

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

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

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

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

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

Loading history...
519
        }
520
521
        $link = $this->AbsoluteLink($action);
522
        $this->extend('updatePreviewLink', $link, $action);
523
        return $link;
524
    }
525
526
    public function getMimeType()
527
    {
528
        return 'text/html';
529
    }
530
531
    /**
532
     * Return the link for this {@link SiteTree} object relative to the SilverStripe root.
533
     *
534
     * By default, if this page is the current home page, and there is no action specified then this will return a link
535
     * to the root of the site. However, if you set the $action parameter to TRUE then the link will not be rewritten
536
     * and returned in its full form.
537
     *
538
     * @uses RootURLController::get_homepage_link()
539
     *
540
     * @param string $action See {@link Link()}
541
     * @return string
542
     */
543
    public function RelativeLink($action = null)
544
    {
545
        if ($this->ParentID && self::config()->nested_urls) {
0 ignored issues
show
Documentation introduced by
The property ParentID does not exist on object<SilverStripe\CMS\Model\SiteTree>. Since you implemented __get, maybe consider adding a @property annotation.

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

<?php

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

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

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

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

}

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

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

See also the PhpDoc documentation for @property.

Loading history...
546
            $parent = $this->Parent();
547
            // If page is removed select parent from version history (for archive page view)
548
            if ((!$parent || !$parent->exists()) && !$this->isOnDraft()) {
0 ignored issues
show
Documentation Bug introduced by
The method isOnDraft does not exist on object<SilverStripe\CMS\Model\SiteTree>? Since you implemented __call, maybe consider adding a @method annotation.

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

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

class ParentClass {
    private $data = array();

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

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

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

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

<?php

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

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

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

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

}

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

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

See also the PhpDoc documentation for @property.

Loading history...
550
            }
551
            $base = $parent->RelativeLink($this->URLSegment);
552
        } 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...
553
            // Unset base for root-level homepages.
554
            // Note: Homepages with action parameters (or $action === true)
555
            // need to retain their URLSegment.
556
            $base = null;
557
        } else {
558
            $base = $this->URLSegment;
559
        }
560
561
        $this->extend('updateRelativeLink', $base, $action);
562
563
        // Legacy support: If $action === true, retain URLSegment for homepages,
564
        // but don't append any action
565
        if ($action === true) {
566
            $action = null;
567
        }
568
569
        return Controller::join_links($base, '/', $action);
570
    }
571
572
    /**
573
     * Get the absolute URL for this page on the Live site.
574
     *
575
     * @param bool $includeStageEqualsLive Whether to append the URL with ?stage=Live to force Live mode
576
     * @return string
577
     */
578
    public function getAbsoluteLiveLink($includeStageEqualsLive = true)
579
    {
580
        $oldReadingMode = Versioned::get_reading_mode();
581
        Versioned::set_stage(Versioned::LIVE);
582
        /** @var SiteTree $live */
583
        $live = Versioned::get_one_by_stage(self::class, Versioned::LIVE, array(
0 ignored issues
show
Documentation introduced by
array('"SiteTree"."ID"' => $this->ID) is of type array<string,integer|str...D\"":"integer|string"}>, but the function expects a string.

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
584
            '"SiteTree"."ID"' => $this->ID
585
        ));
586
        if ($live) {
587
            $link = $live->AbsoluteLink();
588
            if ($includeStageEqualsLive) {
589
                $link = Controller::join_links($link, '?stage=Live');
590
            }
591
        } else {
592
            $link = null;
593
        }
594
595
        Versioned::set_reading_mode($oldReadingMode);
596
        return $link;
597
    }
598
599
    /**
600
     * Generates a link to edit this page in the CMS.
601
     *
602
     * @return string
603
     */
604
    public function CMSEditLink()
605
    {
606
        $link = Controller::join_links(
607
            CMSPageEditController::singleton()->Link('show'),
608
            $this->ID
609
        );
610
        return Director::absoluteURL($link);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The expression \SilverStripe\Control\Di...or::absoluteURL($link); of type string|false adds false to the return on line 610 which is incompatible with the return type declared by the interface SilverStripe\Admin\CMSPreviewable::CMSEditLink of type string. It seems like you forgot to handle an error condition.
Loading history...
611
    }
612
613
614
    /**
615
     * Return a CSS identifier generated from this page's link.
616
     *
617
     * @return string The URL segment
618
     */
619
    public function ElementName()
620
    {
621
        return str_replace('/', '-', trim($this->RelativeLink(true), '/'));
0 ignored issues
show
Documentation introduced by
true is of type boolean, but the function expects a string|null.

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
622
    }
623
624
    /**
625
     * Returns true if this is the currently active page being used to handle this request.
626
     *
627
     * @return bool
628
     */
629
    public function isCurrent()
630
    {
631
        $currentPage = Director::get_current_page();
632
        if ($currentPage instanceof ContentController) {
633
            $currentPage = $currentPage->data();
634
        }
635
        if ($currentPage instanceof SiteTree) {
636
            return $currentPage === $this || $currentPage->ID === $this->ID;
637
        }
638
        return false;
639
    }
640
641
    /**
642
     * Check if this page is in the currently active section (e.g. it is either current or one of its children is
643
     * currently being viewed).
644
     *
645
     * @return bool
646
     */
647
    public function isSection()
648
    {
649
        return $this->isCurrent() || (
650
            Director::get_current_page() instanceof SiteTree && in_array($this->ID, Director::get_current_page()->getAncestors()->column())
651
        );
652
    }
653
654
    /**
655
     * Check if the parent of this page has been removed (or made otherwise unavailable), and is still referenced by
656
     * this child. Any such orphaned page may still require access via the CMS, but should not be shown as accessible
657
     * to external users.
658
     *
659
     * @return bool
660
     */
661
    public function isOrphaned()
662
    {
663
        // Always false for root pages
664
        if (empty($this->ParentID)) {
0 ignored issues
show
Documentation introduced by
The property ParentID does not exist on object<SilverStripe\CMS\Model\SiteTree>. Since you implemented __get, maybe consider adding a @property annotation.

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

<?php

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

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

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

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

}

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

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

See also the PhpDoc documentation for @property.

Loading history...
665
            return false;
666
        }
667
668
        // Parent must exist and not be an orphan itself
669
        $parent = $this->Parent();
670
        return !$parent || !$parent->exists() || $parent->isOrphaned();
671
    }
672
673
    /**
674
     * Return "link" or "current" depending on if this is the {@link SiteTree::isCurrent()} current page.
675
     *
676
     * @return string
677
     */
678
    public function LinkOrCurrent()
679
    {
680
        return $this->isCurrent() ? 'current' : 'link';
681
    }
682
683
    /**
684
     * Return "link" or "section" depending on if this is the {@link SiteTree::isSeciton()} current section.
685
     *
686
     * @return string
687
     */
688
    public function LinkOrSection()
689
    {
690
        return $this->isSection() ? 'section' : 'link';
691
    }
692
693
    /**
694
     * Return "link", "current" or "section" depending on if this page is the current page, or not on the current page
695
     * but in the current section.
696
     *
697
     * @return string
698
     */
699
    public function LinkingMode()
700
    {
701
        if ($this->isCurrent()) {
702
            return 'current';
703
        } elseif ($this->isSection()) {
704
            return 'section';
705
        } else {
706
            return 'link';
707
        }
708
    }
709
710
    /**
711
     * Check if this page is in the given current section.
712
     *
713
     * @param string $sectionName Name of the section to check
714
     * @return bool True if we are in the given section
715
     */
716
    public function InSection($sectionName)
717
    {
718
        $page = Director::get_current_page();
719
        while ($page && $page->exists()) {
720
            if ($sectionName == $page->URLSegment) {
721
                return true;
722
            }
723
            $page = $page->Parent();
724
        }
725
        return false;
726
    }
727
728
    /**
729
     * Reset Sort on duped page
730
     *
731
     * @param SiteTree $original
732
     * @param bool $doWrite
733
     */
734
    public function onBeforeDuplicate($original, $doWrite)
0 ignored issues
show
Unused Code introduced by
The parameter $original is not used and could be removed.

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

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

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

Loading history...
735
    {
736
        $this->Sort = 0;
737
    }
738
739
    /**
740
     * Duplicates each child of this node recursively and returns the top-level duplicate node.
741
     *
742
     * @return static The duplicated object
743
     */
744
    public function duplicateWithChildren()
745
    {
746
        /** @var SiteTree $clone */
747
        $clone = $this->duplicate();
748
        $children = $this->AllChildren();
0 ignored issues
show
Documentation Bug introduced by
The method AllChildren does not exist on object<SilverStripe\CMS\Model\SiteTree>? Since you implemented __call, maybe consider adding a @method annotation.

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

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

class ParentClass {
    private $data = array();

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

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

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
749
750
        if ($children) {
751
            /** @var SiteTree $child */
752
            $sort = 0;
753
            foreach ($children as $child) {
754
                $childClone = $child->duplicateWithChildren();
755
                $childClone->ParentID = $clone->ID;
756
                //retain sort order by manually setting sort values
757
                $childClone->Sort = ++$sort;
758
                $childClone->write();
759
            }
760
        }
761
762
        return $clone;
763
    }
764
765
    /**
766
     * Duplicate this node and its children as a child of the node with the given ID
767
     *
768
     * @param int $id ID of the new node's new parent
769
     */
770
    public function duplicateAsChild($id)
771
    {
772
        /** @var SiteTree $newSiteTree */
773
        $newSiteTree = $this->duplicate();
774
        $newSiteTree->ParentID = $id;
0 ignored issues
show
Documentation introduced by
The property ParentID does not exist on object<SilverStripe\CMS\Model\SiteTree>. Since you implemented __set, maybe consider adding a @property annotation.

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

<?php

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

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

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

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

}

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

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

See also the PhpDoc documentation for @property.

Loading history...
775
        $newSiteTree->Sort = 0;
776
        $newSiteTree->write();
777
    }
778
779
    /**
780
     * Return a breadcrumb trail to this page. Excludes "hidden" pages (with ShowInMenus=0) by default.
781
     *
782
     * @param int $maxDepth The maximum depth to traverse.
783
     * @param boolean $unlinked Whether to link page titles.
784
     * @param boolean|string $stopAtPageType ClassName of a page to stop the upwards traversal.
785
     * @param boolean $showHidden Include pages marked with the attribute ShowInMenus = 0
786
     * @return string The breadcrumb trail.
787
     */
788
    public function Breadcrumbs($maxDepth = 20, $unlinked = false, $stopAtPageType = false, $showHidden = false)
789
    {
790
        $pages = $this->getBreadcrumbItems($maxDepth, $stopAtPageType, $showHidden);
791
        $template = new SSViewer('BreadcrumbsTemplate');
792
        return $template->process($this->customise(new ArrayData(array(
793
            "Pages" => $pages,
794
            "Unlinked" => $unlinked
795
        ))));
796
    }
797
798
799
    /**
800
     * Returns a list of breadcrumbs for the current page.
801
     *
802
     * @param int $maxDepth The maximum depth to traverse.
803
     * @param boolean|string $stopAtPageType ClassName of a page to stop the upwards traversal.
804
     * @param boolean $showHidden Include pages marked with the attribute ShowInMenus = 0
805
     *
806
     * @return ArrayList
807
    */
808
    public function getBreadcrumbItems($maxDepth = 20, $stopAtPageType = false, $showHidden = false)
809
    {
810
        $page = $this;
811
        $pages = array();
812
813
        while ($page
814
            && $page->exists()
815
            && (!$maxDepth || count($pages) < $maxDepth)
816
            && (!$stopAtPageType || $page->ClassName != $stopAtPageType)
817
        ) {
818
            if ($showHidden || $page->ShowInMenus || ($page->ID == $this->ID)) {
819
                $pages[] = $page;
820
            }
821
822
            $page = $page->Parent();
823
        }
824
825
        return new ArrayList(array_reverse($pages));
826
    }
827
828
829
    /**
830
     * Make this page a child of another page.
831
     *
832
     * If the parent page does not exist, resolve it to a valid ID before updating this page's reference.
833
     *
834
     * @param SiteTree|int $item Either the parent object, or the parent ID
835
     */
836
    public function setParent($item)
837
    {
838
        if (is_object($item)) {
839
            if (!$item->exists()) {
840
                $item->write();
841
            }
842
            $this->setField("ParentID", $item->ID);
843
        } else {
844
            $this->setField("ParentID", $item);
845
        }
846
    }
847
848
    /**
849
     * Get the parent of this page.
850
     *
851
     * @return SiteTree Parent of this page
852
     */
853
    public function getParent()
854
    {
855
        if ($parentID = $this->getField("ParentID")) {
856
            return DataObject::get_by_id("SilverStripe\\CMS\\Model\\SiteTree", $parentID);
857
        }
858
        return null;
859
    }
860
861
    /**
862
     * Return a string of the form "parent - page" or "grandparent - parent - page" using page titles
863
     *
864
     * @param int $level The maximum amount of levels to traverse.
865
     * @param string $separator Seperating string
866
     * @return string The resulting string
867
     */
868
    public function NestedTitle($level = 2, $separator = " - ")
869
    {
870
        $item = $this;
871
        $parts = [];
872
        while ($item && $level > 0) {
873
            $parts[] = $item->Title;
874
            $item = $item->getParent();
875
            $level--;
876
        }
877
        return implode($separator, array_reverse($parts));
878
    }
879
880
    /**
881
     * This function should return true if the current user can execute this action. It can be overloaded to customise
882
     * the security model for an application.
883
     *
884
     * Slightly altered from parent behaviour in {@link DataObject->can()}:
885
     * - Checks for existence of a method named "can<$perm>()" on the object
886
     * - Calls decorators and only returns for FALSE "vetoes"
887
     * - Falls back to {@link Permission::check()}
888
     * - Does NOT check for many-many relations named "Can<$perm>"
889
     *
890
     * @uses DataObjectDecorator->can()
891
     *
892
     * @param string $perm The permission to be checked, such as 'View'
893
     * @param Member $member The member whose permissions need checking. Defaults to the currently logged in user.
894
     * @param array $context Context argument for canCreate()
895
     * @return bool True if the the member is allowed to do the given action
896
     */
897
    public function can($perm, $member = null, $context = array())
898
    {
899 View Code Duplication
        if (!$member || !($member instanceof Member) || is_numeric($member)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
900
            $member = Member::currentUserID();
901
        }
902
903
        if ($member && Permission::checkMember($member, "ADMIN")) {
904
            return true;
905
        }
906
907
        if (is_string($perm) && method_exists($this, 'can' . ucfirst($perm))) {
908
            $method = 'can' . ucfirst($perm);
909
            return $this->$method($member);
910
        }
911
912
        $results = $this->extend('can', $member);
913
        if ($results && is_array($results)) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $results of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
914
            if (!min($results)) {
915
                return false;
916
            }
917
        }
918
919
        return ($member && Permission::checkMember($member, $perm));
920
    }
921
922
    /**
923
     * This function should return true if the current user can add children to this page. It can be overloaded to
924
     * customise the security model for an application.
925
     *
926
     * Denies permission if any of the following conditions is true:
927
     * - alternateCanAddChildren() on a extension returns false
928
     * - canEdit() is not granted
929
     * - There are no classes defined in {@link $allowed_children}
930
     *
931
     * @uses SiteTreeExtension->canAddChildren()
932
     * @uses canEdit()
933
     * @uses $allowed_children
934
     *
935
     * @param Member|int $member
936
     * @return bool True if the current user can add children
937
     */
938
    public function canAddChildren($member = null)
939
    {
940
        // Disable adding children to archived pages
941
        if (!$this->isOnDraft()) {
0 ignored issues
show
Documentation Bug introduced by
The method isOnDraft does not exist on object<SilverStripe\CMS\Model\SiteTree>? Since you implemented __call, maybe consider adding a @method annotation.

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

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

class ParentClass {
    private $data = array();

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

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

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
942
            return false;
943
        }
944
945 View Code Duplication
        if (!$member || !($member instanceof Member) || is_numeric($member)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
946
            $member = Member::currentUserID();
947
        }
948
949
        // Standard mechanism for accepting permission changes from extensions
950
        $extended = $this->extendedCan('canAddChildren', $member);
951
        if ($extended !== null) {
952
            return $extended;
953
        }
954
955
        // Default permissions
956
        if ($member && Permission::checkMember($member, "ADMIN")) {
957
            return true;
958
        }
959
960
        return $this->canEdit($member) && $this->stat('allowed_children') != 'none';
961
    }
962
963
    /**
964
     * This function should return true if the current user can view this page. It can be overloaded to customise the
965
     * security model for an application.
966
     *
967
     * Denies permission if any of the following conditions is true:
968
     * - canView() on any extension returns false
969
     * - "CanViewType" directive is set to "Inherit" and any parent page return false for canView()
970
     * - "CanViewType" directive is set to "LoggedInUsers" and no user is logged in
971
     * - "CanViewType" directive is set to "OnlyTheseUsers" and user is not in the given groups
972
     *
973
     * @uses DataExtension->canView()
974
     * @uses ViewerGroups()
975
     *
976
     * @param Member|int $member
977
     * @return bool True if the current user can view this page
978
     */
979
    public function canView($member = null)
980
    {
981 View Code Duplication
        if (!$member || !($member instanceof Member) || is_numeric($member)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
982
            $member = Member::currentUserID();
983
        }
984
985
        // Standard mechanism for accepting permission changes from extensions
986
        $extended = $this->extendedCan('canView', $member);
987
        if ($extended !== null) {
988
            return $extended;
989
        }
990
991
        // admin override
992
        if ($member && Permission::checkMember($member, array("ADMIN", "SITETREE_VIEW_ALL"))) {
993
            return true;
994
        }
995
996
        // Orphaned pages (in the current stage) are unavailable, except for admins via the CMS
997
        if ($this->isOrphaned()) {
998
            return false;
999
        }
1000
1001
        // check for empty spec
1002
        if (!$this->CanViewType || $this->CanViewType == 'Anyone') {
1003
            return true;
1004
        }
1005
1006
        // check for inherit
1007
        if ($this->CanViewType == 'Inherit') {
1008
            if ($this->ParentID) {
0 ignored issues
show
Documentation introduced by
The property ParentID does not exist on object<SilverStripe\CMS\Model\SiteTree>. Since you implemented __get, maybe consider adding a @property annotation.

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

<?php

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

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

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

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

}

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

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

See also the PhpDoc documentation for @property.

Loading history...
1009
                return $this->Parent()->canView($member);
1010
            } else {
1011
                return $this->getSiteConfig()->canViewPages($member);
1012
            }
1013
        }
1014
1015
        // check for any logged-in users
1016
        if ($this->CanViewType == 'LoggedInUsers' && $member) {
1017
            return true;
1018
        }
1019
1020
        // check for specific groups
1021
        if ($member && is_numeric($member)) {
1022
            $member = DataObject::get_by_id('SilverStripe\\Security\\Member', $member);
1023
        }
1024
        if ($this->CanViewType == 'OnlyTheseUsers'
1025
            && $member
1026
            && $member->inGroups($this->ViewerGroups())
1027
        ) {
1028
            return true;
1029
        }
1030
1031
        return false;
1032
    }
1033
1034
    /**
1035
     * Check if this page can be published
1036
     *
1037
     * @param Member $member
1038
     * @return bool
1039
     */
1040
    public function canPublish($member = null)
1041
    {
1042
        if (!$member) {
1043
            $member = Member::currentUser();
1044
        }
1045
1046
        // Check extension
1047
        $extended = $this->extendedCan('canPublish', $member);
0 ignored issues
show
Bug introduced by
It seems like $member can be null; however, extendedCan() does not accept null, maybe add an additional type check?

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

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

function doesNotAcceptNull(stdClass $x) { }

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

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

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

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

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

    return array();
}

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

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

Loading history...
1299
                }
1300
                return $cachedValues;
1301
            }
1302
1303
            // If a member doesn't have a certain permission then they can't edit anything
1304
            if (!$memberID || ($globalPermission && !Permission::checkMember($memberID, $globalPermission))) {
1305
                return $result;
1306
            }
1307
1308
            // Placeholder for parameterised ID list
1309
            $idPlaceholders = DB::placeholders($ids);
1310
1311
            // If page can't be viewed, don't grant edit permissions to do - implement can_view_multiple(), so this can
1312
            // be enabled
1313
            //$ids = array_keys(array_filter(self::can_view_multiple($ids, $memberID)));
1314
1315
            // Get the groups that the given member belongs to
1316
            /** @var Member $member */
1317
            $member = DataObject::get_by_id('SilverStripe\\Security\\Member', $memberID);
1318
            $groupIDs = $member->Groups()->column("ID");
1319
            $SQL_groupList = implode(", ", $groupIDs);
1320
            if (!$SQL_groupList) {
1321
                $SQL_groupList = '0';
1322
            }
1323
1324
            $combinedStageResult = array();
1325
1326
            foreach (array(Versioned::DRAFT, Versioned::LIVE) as $stage) {
1327
                // Start by filling the array with the pages that actually exist
1328
                /** @skipUpgrade */
1329
                $table = ($stage=='Stage') ? "SiteTree" : "SiteTree_$stage";
1330
1331
                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...
1332
                    $idQuery = "SELECT \"ID\" FROM \"$table\" WHERE \"ID\" IN ($idPlaceholders)";
1333
                    $stageIds = DB::prepared_query($idQuery, $ids)->column();
1334
                } else {
1335
                    $stageIds = array();
1336
                }
1337
                $result = array_fill_keys($stageIds, false);
1338
1339
                // Get the uninherited permissions
1340
                $uninheritedPermissions = Versioned::get_by_stage("SilverStripe\\CMS\\Model\\SiteTree", $stage)
1341
                    ->where(array(
1342
                        "(\"$typeField\" = 'LoggedInUsers' OR
1343
						(\"$typeField\" = 'OnlyTheseUsers' AND \"$groupJoinTable\".\"SiteTreeID\" IS NOT NULL))
1344
						AND \"SiteTree\".\"ID\" IN ($idPlaceholders)"
1345
                        => $ids
1346
                    ))
1347
                    ->leftJoin($groupJoinTable, "\"$groupJoinTable\".\"SiteTreeID\" = \"SiteTree\".\"ID\" AND \"$groupJoinTable\".\"GroupID\" IN ($SQL_groupList)");
1348
1349
                if ($uninheritedPermissions) {
1350
                    // Set all the relevant items in $result to true
1351
                    $result = array_fill_keys($uninheritedPermissions->column('ID'), true) + $result;
1352
                }
1353
1354
                // Get permissions that are inherited
1355
                $potentiallyInherited = Versioned::get_by_stage(
1356
                    "SilverStripe\\CMS\\Model\\SiteTree",
1357
                    $stage,
1358
                    array("\"$typeField\" = 'Inherit' AND \"SiteTree\".\"ID\" IN ($idPlaceholders)" => $ids)
0 ignored issues
show
Documentation introduced by
array("\"{$typeField}\" ...laceholders})" => $ids) is of type array<string|integer,array>, but the function expects a string.

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1359
                );
1360
1361
                if ($potentiallyInherited) {
1362
                    // Group $potentiallyInherited by ParentID; we'll look at the permission of all those parents and
1363
                    // then see which ones the user has permission on
1364
                    $groupedByParent = array();
1365
                    foreach ($potentiallyInherited as $item) {
1366
                        /** @var SiteTree $item */
1367
                        if ($item->ParentID) {
0 ignored issues
show
Documentation introduced by
The property ParentID does not exist on object<SilverStripe\CMS\Model\SiteTree>. Since you implemented __get, maybe consider adding a @property annotation.

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

<?php

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

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

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

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

}

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

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

See also the PhpDoc documentation for @property.

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

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

<?php

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

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

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

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

}

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

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

See also the PhpDoc documentation for @property.

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

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

<?php

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

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

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

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

}

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

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

See also the PhpDoc documentation for @property.

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

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

<?php

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

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

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

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

}

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

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

See also the PhpDoc documentation for @property.

Loading history...
1372
                        } else {
1373
                            // Might return different site config based on record context, e.g. when subsites module
1374
                            // is used
1375
                            $siteConfig = $item->getSiteConfig();
1376
                            $result[$item->ID] = $siteConfig->{$siteConfigMethod}($memberID);
1377
                        }
1378
                    }
1379
1380
                    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...
1381
                        $actuallyInherited = self::batch_permission_check(array_keys($groupedByParent), $memberID, $typeField, $groupJoinTable, $siteConfigMethod);
1382
                        if ($actuallyInherited) {
1383
                            $parentIDs = array_keys(array_filter($actuallyInherited));
1384
                            foreach ($parentIDs as $parentID) {
1385
                                // Set all the relevant items in $result to true
1386
                                $result = array_fill_keys($groupedByParent[$parentID], true) + $result;
1387
                            }
1388
                        }
1389
                    }
1390
                }
1391
1392
                $combinedStageResult = $combinedStageResult + $result;
1393
            }
1394
        }
1395
1396
        if (isset($combinedStageResult)) {
1397
            // Cache the results
1398
            if (empty(self::$cache_permissions[$cacheKey])) {
1399
                self::$cache_permissions[$cacheKey] = array();
1400
            }
1401
            self::$cache_permissions[$cacheKey] = $combinedStageResult + self::$cache_permissions[$cacheKey];
1402
            return $combinedStageResult;
1403
        } else {
1404
            return array();
1405
        }
1406
    }
1407
1408
    /**
1409
     * Get the 'can edit' information for a number of SiteTree pages.
1410
     *
1411
     * @param array $ids       An array of IDs of the SiteTree pages to look up
1412
     * @param int   $memberID  ID of member
1413
     * @param bool  $useCached Return values from the permission cache if they exist
1414
     * @return array A map where the IDs are keys and the values are booleans stating whether the given page can be
1415
     *                         edited
1416
     */
1417
    public static function can_edit_multiple($ids, $memberID, $useCached = true)
1418
    {
1419
        return self::batch_permission_check($ids, $memberID, 'CanEditType', 'SiteTree_EditorGroups', 'canEditPages', null, $useCached);
1420
    }
1421
1422
    /**
1423
     * Get the 'can edit' information for a number of SiteTree pages.
1424
     *
1425
     * @param array $ids       An array of IDs of the SiteTree pages to look up
1426
     * @param int   $memberID  ID of member
1427
     * @param bool  $useCached Return values from the permission cache if they exist
1428
     * @return array
1429
     */
1430
    public static function can_delete_multiple($ids, $memberID, $useCached = true)
1431
    {
1432
        $deletable = array();
1433
        $result = array_fill_keys($ids, false);
1434
        $cacheKey = "delete-$memberID";
1435
1436
        // Look in the cache for values
1437
        if ($useCached && isset(self::$cache_permissions[$cacheKey])) {
1438
            $cachedValues = array_intersect_key(self::$cache_permissions[$cacheKey], $result);
1439
1440
            // If we can't find everything in the cache, then look up the remainder separately
1441
            $uncachedValues = array_diff_key($result, self::$cache_permissions[$cacheKey]);
1442
            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...
1443
                $cachedValues = self::can_delete_multiple(array_keys($uncachedValues), $memberID, false)
1444
                    + $cachedValues;
1445
            }
1446
            return $cachedValues;
1447
        }
1448
1449
        // You can only delete pages that you can edit
1450
        $editableIDs = array_keys(array_filter(self::can_edit_multiple($ids, $memberID)));
1451
        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...
1452
            // You can only delete pages whose children you can delete
1453
            $editablePlaceholders = DB::placeholders($editableIDs);
1454
            $childRecords = SiteTree::get()->where(array(
1455
                "\"SiteTree\".\"ParentID\" IN ($editablePlaceholders)" => $editableIDs
1456
            ));
1457
            if ($childRecords) {
1458
                $children = $childRecords->map("ID", "ParentID");
1459
1460
                // Find out the children that can be deleted
1461
                $deletableChildren = self::can_delete_multiple($children->keys(), $memberID);
1462
1463
                // Get a list of all the parents that have no undeletable children
1464
                $deletableParents = array_fill_keys($editableIDs, true);
1465
                foreach ($deletableChildren as $id => $canDelete) {
1466
                    if (!$canDelete) {
1467
                        unset($deletableParents[$children[$id]]);
1468
                    }
1469
                }
1470
1471
                // Use that to filter the list of deletable parents that have children
1472
                $deletableParents = array_keys($deletableParents);
1473
1474
                // Also get the $ids that don't have children
1475
                $parents = array_unique($children->values());
1476
                $deletableLeafNodes = array_diff($editableIDs, $parents);
1477
1478
                // Combine the two
1479
                $deletable = array_merge($deletableParents, $deletableLeafNodes);
1480
            } else {
1481
                $deletable = $editableIDs;
1482
            }
1483
        }
1484
1485
        // Convert the array of deletable IDs into a map of the original IDs with true/false as the value
1486
        return array_fill_keys($deletable, true) + array_fill_keys($ids, false);
1487
    }
1488
1489
    /**
1490
     * Collate selected descendants of this page.
1491
     *
1492
     * {@link $condition} will be evaluated on each descendant, and if it is succeeds, that item will be added to the
1493
     * $collator array.
1494
     *
1495
     * @param string $condition The PHP condition to be evaluated. The page will be called $item
1496
     * @param array  $collator  An array, passed by reference, to collect all of the matching descendants.
1497
     * @return bool
1498
     */
1499
    public function collateDescendants($condition, &$collator)
1500
    {
1501
        $children = $this->Children();
0 ignored issues
show
Bug introduced by
The method Children() does not exist on SilverStripe\CMS\Model\SiteTree. Did you maybe mean duplicateWithChildren()?

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

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

Loading history...
1502
        if ($children) {
1503
            foreach ($children as $item) {
1504
                if (eval("return $condition;")) {
1505
                    $collator[] = $item;
1506
                }
1507
                /** @var SiteTree $item */
1508
                $item->collateDescendants($condition, $collator);
1509
            }
1510
            return true;
1511
        }
1512
        return false;
1513
    }
1514
1515
    /**
1516
     * Return the title, description, keywords and language metatags.
1517
     *
1518
     * @todo Move <title> tag in separate getter for easier customization and more obvious usage
1519
     *
1520
     * @param bool $includeTitle Show default <title>-tag, set to false for custom templating
1521
     * @return string The XHTML metatags
1522
     */
1523
    public function MetaTags($includeTitle = true)
1524
    {
1525
        $tags = array();
1526
        if ($includeTitle && strtolower($includeTitle) != 'false') {
1527
            $tags[] = FormField::create_tag('title', array(), $this->obj('Title')->forTemplate());
1528
        }
1529
1530
        $generator = trim(Config::inst()->get(self::class, 'meta_generator'));
1531
        if (!empty($generator)) {
1532
            $tags[] = FormField::create_tag('meta', array(
1533
                'name' => 'generator',
1534
                'content' => $generator,
1535
            ));
1536
        }
1537
1538
        $charset = Config::inst()->get('SilverStripe\\Control\\ContentNegotiator', 'encoding');
1539
        $tags[] = FormField::create_tag('meta', array(
1540
            'http-equiv' => 'Content-Type',
1541
            'content' => 'text/html; charset=' . $charset,
1542
        ));
1543
        if ($this->MetaDescription) {
1544
            $tags[] = FormField::create_tag('meta', array(
1545
                'name' => 'description',
1546
                'content' => $this->MetaDescription,
1547
            ));
1548
        }
1549
1550
        if (Permission::check('CMS_ACCESS_CMSMain')
1551
            && !$this instanceof ErrorPage
1552
            && $this->ID > 0
1553
        ) {
1554
            $tags[] = FormField::create_tag('meta', array(
1555
                'name' => 'x-page-id',
1556
                'content' => $this->obj('ID')->forTemplate(),
1557
            ));
1558
            $tags[] = FormField::create_tag('meta', array(
1559
                'name' => 'x-cms-edit-link',
1560
                'content' => $this->obj('CMSEditLink')->forTemplate(),
1561
            ));
1562
        }
1563
1564
        $tags = implode("\n", $tags);
1565
        if ($this->ExtraMeta) {
1566
            $tags .= $this->obj('ExtraMeta')->forTemplate();
1567
        }
1568
1569
        $this->extend('MetaTags', $tags);
1570
1571
        return $tags;
1572
    }
1573
1574
    /**
1575
     * Returns the object that contains the content that a user would associate with this page.
1576
     *
1577
     * Ordinarily, this is just the page itself, but for example on RedirectorPages or VirtualPages ContentSource() will
1578
     * return the page that is linked to.
1579
     *
1580
     * @return $this
1581
     */
1582
    public function ContentSource()
1583
    {
1584
        return $this;
1585
    }
1586
1587
    /**
1588
     * Add default records to database.
1589
     *
1590
     * This function is called whenever the database is built, after the database tables have all been created. Overload
1591
     * this to add default records when the database is built, but make sure you call parent::requireDefaultRecords().
1592
     */
1593
    public function requireDefaultRecords()
1594
    {
1595
        parent::requireDefaultRecords();
1596
1597
        // default pages
1598
        if (static::class == self::class && $this->config()->create_default_pages) {
1599
            if (!SiteTree::get_by_link(RootURLController::config()->default_homepage_link)) {
1600
                $homepage = new Page();
1601
                $homepage->Title = _t('SiteTree.DEFAULTHOMETITLE', 'Home');
1602
                $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>');
1603
                $homepage->URLSegment = RootURLController::config()->default_homepage_link;
1604
                $homepage->Sort = 1;
1605
                $homepage->write();
1606
                $homepage->copyVersionToStage(Versioned::DRAFT, Versioned::LIVE);
1607
                $homepage->flushCache();
1608
                DB::alteration_message('Home page created', 'created');
1609
            }
1610
1611
            if (DB::query("SELECT COUNT(*) FROM \"SiteTree\"")->value() == 1) {
1612
                $aboutus = new Page();
1613
                $aboutus->Title = _t('SiteTree.DEFAULTABOUTTITLE', 'About Us');
1614
                $aboutus->Content = _t(
1615
                    'SiteTree.DEFAULTABOUTCONTENT',
1616
                    '<p>You can fill this page out with your own content, or delete it and create your own pages.</p>'
1617
                );
1618
                $aboutus->Sort = 2;
1619
                $aboutus->write();
1620
                $aboutus->copyVersionToStage(Versioned::DRAFT, Versioned::LIVE);
1621
                $aboutus->flushCache();
1622
                DB::alteration_message('About Us page created', 'created');
1623
1624
                $contactus = new Page();
1625
                $contactus->Title = _t('SiteTree.DEFAULTCONTACTTITLE', 'Contact Us');
1626
                $contactus->Content = _t(
1627
                    'SiteTree.DEFAULTCONTACTCONTENT',
1628
                    '<p>You can fill this page out with your own content, or delete it and create your own pages.</p>'
1629
                );
1630
                $contactus->Sort = 3;
1631
                $contactus->write();
1632
                $contactus->copyVersionToStage(Versioned::DRAFT, Versioned::LIVE);
1633
                $contactus->flushCache();
1634
                DB::alteration_message('Contact Us page created', 'created');
1635
            }
1636
        }
1637
    }
1638
1639
    protected function onBeforeWrite()
1640
    {
1641
        parent::onBeforeWrite();
1642
1643
        // If Sort hasn't been set, make this page come after it's siblings
1644
        if (!$this->Sort) {
1645
            $parentID = ($this->ParentID) ? $this->ParentID : 0;
0 ignored issues
show
Documentation introduced by
The property ParentID does not exist on object<SilverStripe\CMS\Model\SiteTree>. Since you implemented __get, maybe consider adding a @property annotation.

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

<?php

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

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

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

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

}

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

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

See also the PhpDoc documentation for @property.

Loading history...
1646
            $this->Sort = DB::prepared_query(
1647
                "SELECT MAX(\"Sort\") + 1 FROM \"SiteTree\" WHERE \"ParentID\" = ?",
1648
                array($parentID)
1649
            )->value();
1650
        }
1651
1652
        // If there is no URLSegment set, generate one from Title
1653
        $defaultSegment = $this->generateURLSegment(_t(
1654
            'CMSMain.NEWPAGE',
1655
            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...
1656
        ));
1657
        if ((!$this->URLSegment || $this->URLSegment == $defaultSegment) && $this->Title) {
1658
            $this->URLSegment = $this->generateURLSegment($this->Title);
1659
        } elseif ($this->isChanged('URLSegment', 2)) {
1660
            // Do a strict check on change level, to avoid double encoding caused by
1661
            // bogus changes through forceChange()
1662
            $filter = URLSegmentFilter::create();
1663
            $this->URLSegment = $filter->filter($this->URLSegment);
1664
            // If after sanitising there is no URLSegment, give it a reasonable default
1665
            if (!$this->URLSegment) {
1666
                $this->URLSegment = "page-$this->ID";
1667
            }
1668
        }
1669
1670
        // Ensure that this object has a non-conflicting URLSegment value.
1671
        $count = 2;
1672
        while (!$this->validURLSegment()) {
1673
            $this->URLSegment = preg_replace('/-[0-9]+$/', null, $this->URLSegment) . '-' . $count;
1674
            $count++;
1675
        }
1676
1677
        $this->syncLinkTracking();
1678
1679
        // Check to see if we've only altered fields that shouldn't affect versioning
1680
        $fieldsIgnoredByVersioning = array('HasBrokenLink', 'Status', 'HasBrokenFile', 'ToDo', 'VersionID', 'SaveCount');
1681
        $changedFields = array_keys($this->getChangedFields(true, 2));
1682
1683
        // This more rigorous check is inline with the test that write() does to decide whether or not to write to the
1684
        // DB. We use that to avoid cluttering the system with a migrateVersion() call that doesn't get used
1685
        $oneChangedFields = array_keys($this->getChangedFields(true, 1));
1686
1687
        if ($oneChangedFields && !array_diff($changedFields, $fieldsIgnoredByVersioning)) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $oneChangedFields of type array<integer|string> is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
1688
            // This will have the affect of preserving the versioning
1689
            $this->migrateVersion($this->Version);
0 ignored issues
show
Bug introduced by
The property Version does not seem to exist. Did you mean versioning?

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

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

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

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

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

class ParentClass {
    private $data = array();

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

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

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
1690
        }
1691
    }
1692
1693
    /**
1694
     * Trigger synchronisation of link tracking
1695
     *
1696
     * {@see SiteTreeLinkTracking::augmentSyncLinkTracking}
1697
     */
1698
    public function syncLinkTracking()
1699
    {
1700
        $this->extend('augmentSyncLinkTracking');
1701
    }
1702
1703
    public function onBeforeDelete()
1704
    {
1705
        parent::onBeforeDelete();
1706
1707
        // If deleting this page, delete all its children.
1708
        if (SiteTree::config()->enforce_strict_hierarchy && $children = $this->AllChildren()) {
0 ignored issues
show
Documentation Bug introduced by
The method AllChildren does not exist on object<SilverStripe\CMS\Model\SiteTree>? Since you implemented __call, maybe consider adding a @method annotation.

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

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

class ParentClass {
    private $data = array();

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

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

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
1709
            foreach ($children as $child) {
1710
                /** @var SiteTree $child */
1711
                $child->delete();
1712
            }
1713
        }
1714
    }
1715
1716
    public function onAfterDelete()
1717
    {
1718
        // Need to flush cache to avoid outdated versionnumber references
1719
        $this->flushCache();
1720
1721
        // Need to mark pages depending to this one as broken
1722
        $dependentPages = $this->DependentPages();
1723
        if ($dependentPages) {
1724
            foreach ($dependentPages as $page) {
1725
                        // $page->write() calls syncLinkTracking, which does all the hard work for us.
1726
                $page->write();
1727
            }
1728
        }
1729
1730
        parent::onAfterDelete();
1731
    }
1732
1733
    public function flushCache($persistent = true)
1734
    {
1735
        parent::flushCache($persistent);
1736
        $this->_cache_statusFlags = null;
1737
    }
1738
1739
    public function validate()
1740
    {
1741
        $result = parent::validate();
1742
1743
        // Allowed children validation
1744
        $parent = $this->getParent();
1745
        if ($parent && $parent->exists()) {
1746
            // No need to check for subclasses or instanceof, as allowedChildren() already
1747
            // deconstructs any inheritance trees already.
1748
            $allowed = $parent->allowedChildren();
1749
            $subject = ($this instanceof VirtualPage && $this->CopyContentFromID)
0 ignored issues
show
Bug introduced by
The property CopyContentFromID does not seem to exist. Did you mean Content?

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

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

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

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

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

class ParentClass {
    private $data = array();

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

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

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
1751
                : $this;
1752
            if (!in_array($subject->ClassName, $allowed)) {
1753
                $result->addError(
1754
                    _t(
1755
                        'SiteTree.PageTypeNotAllowed',
1756
                        'Page type "{type}" not allowed as child of this parent page',
1757
                        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...
1758
                    ),
1759
                    ValidationResult::TYPE_ERROR,
1760
                    'ALLOWED_CHILDREN'
1761
                );
1762
            }
1763
        }
1764
1765
        // "Can be root" validation
1766
        if (!$this->stat('can_be_root') && !$this->ParentID) {
0 ignored issues
show
Documentation introduced by
The property ParentID does not exist on object<SilverStripe\CMS\Model\SiteTree>. Since you implemented __get, maybe consider adding a @property annotation.

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

<?php

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

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

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

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

}

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

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

See also the PhpDoc documentation for @property.

Loading history...
1767
            $result->addError(
1768
                _t(
1769
                    'SiteTree.PageTypNotAllowedOnRoot',
1770
                    'Page type "{type}" is not allowed on the root level',
1771
                    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...
1772
                ),
1773
                ValidationResult::TYPE_ERROR,
1774
                'CAN_BE_ROOT'
1775
            );
1776
        }
1777
1778
        return $result;
1779
    }
1780
1781
    /**
1782
     * Returns true if this object has a URLSegment value that does not conflict with any other objects. This method
1783
     * checks for:
1784
     *  - A page with the same URLSegment that has a conflict
1785
     *  - Conflicts with actions on the parent page
1786
     *  - A conflict caused by a root page having the same URLSegment as a class name
1787
     *
1788
     * @return bool
1789
     */
1790
    public function validURLSegment()
1791
    {
1792
        if (self::config()->nested_urls && $parent = $this->Parent()) {
1793
            if ($controller = ModelAsController::controller_for($parent)) {
1794
                if ($controller instanceof Controller && $controller->hasAction($this->URLSegment)) {
1795
                    return false;
1796
                }
1797
            }
1798
        }
1799
1800
        if (!self::config()->nested_urls || !$this->ParentID) {
0 ignored issues
show
Documentation introduced by
The property ParentID does not exist on object<SilverStripe\CMS\Model\SiteTree>. Since you implemented __get, maybe consider adding a @property annotation.

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

<?php

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

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

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

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

}

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

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

See also the PhpDoc documentation for @property.

Loading history...
1801
            if (class_exists($this->URLSegment) && is_subclass_of($this->URLSegment, 'SilverStripe\\Control\\RequestHandler')) {
1802
                return false;
1803
            }
1804
        }
1805
1806
        // Filters by url, id, and parent
1807
        $filter = array('"SiteTree"."URLSegment"' => $this->URLSegment);
1808
        if ($this->ID) {
1809
            $filter['"SiteTree"."ID" <> ?'] = $this->ID;
1810
        }
1811
        if (self::config()->nested_urls) {
1812
            $filter['"SiteTree"."ParentID"'] = $this->ParentID ? $this->ParentID : 0;
0 ignored issues
show
Documentation introduced by
The property ParentID does not exist on object<SilverStripe\CMS\Model\SiteTree>. Since you implemented __get, maybe consider adding a @property annotation.

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

<?php

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

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

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

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

}

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

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

See also the PhpDoc documentation for @property.

Loading history...
1813
        }
1814
1815
        // If any of the extensions return `0` consider the segment invalid
1816
        $extensionResponses = array_filter(
1817
            (array)$this->extend('augmentValidURLSegment'),
1818
            function ($response) {
1819
                return !is_null($response);
1820
            }
1821
        );
1822
        if ($extensionResponses) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $extensionResponses 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...
1823
            return min($extensionResponses);
1824
        }
1825
1826
        // Check existence
1827
        return !DataObject::get(self::class, $filter)->exists();
1828
    }
1829
1830
    /**
1831
     * Generate a URL segment based on the title provided.
1832
     *
1833
     * If {@link Extension}s wish to alter URL segment generation, they can do so by defining
1834
     * updateURLSegment(&$url, $title).  $url will be passed by reference and should be modified. $title will contain
1835
     * the title that was originally used as the source of this generated URL. This lets extensions either start from
1836
     * scratch, or incrementally modify the generated URL.
1837
     *
1838
     * @param string $title Page title
1839
     * @return string Generated url segment
1840
     */
1841
    public function generateURLSegment($title)
1842
    {
1843
        $filter = URLSegmentFilter::create();
1844
        $t = $filter->filter($title);
1845
1846
        // Fallback to generic page name if path is empty (= no valid, convertable characters)
1847
        if (!$t || $t == '-' || $t == '-1') {
1848
            $t = "page-$this->ID";
1849
        }
1850
1851
        // Hook for extensions
1852
        $this->extend('updateURLSegment', $t, $title);
1853
1854
        return $t;
1855
    }
1856
1857
    /**
1858
     * Gets the URL segment for the latest draft version of this page.
1859
     *
1860
     * @return string
1861
     */
1862
    public function getStageURLSegment()
1863
    {
1864
        $stageRecord = Versioned::get_one_by_stage(self::class, Versioned::DRAFT, array(
0 ignored issues
show
Documentation introduced by
array('"SiteTree"."ID"' => $this->ID) is of type array<string,integer|str...D\"":"integer|string"}>, but the function expects a string.

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1865
            '"SiteTree"."ID"' => $this->ID
1866
        ));
1867
        return ($stageRecord) ? $stageRecord->URLSegment : null;
1868
    }
1869
1870
    /**
1871
     * Gets the URL segment for the currently published version of this page.
1872
     *
1873
     * @return string
1874
     */
1875
    public function getLiveURLSegment()
1876
    {
1877
        $liveRecord = Versioned::get_one_by_stage(self::class, Versioned::LIVE, array(
0 ignored issues
show
Documentation introduced by
array('"SiteTree"."ID"' => $this->ID) is of type array<string,integer|str...D\"":"integer|string"}>, but the function expects a string.

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1878
            '"SiteTree"."ID"' => $this->ID
1879
        ));
1880
        return ($liveRecord) ? $liveRecord->URLSegment : null;
1881
    }
1882
1883
    /**
1884
     * Returns the pages that depend on this page. This includes virtual pages, pages that link to it, etc.
1885
     *
1886
     * @param bool $includeVirtuals Set to false to exlcude virtual pages.
1887
     * @return ArrayList
1888
     */
1889
    public function DependentPages($includeVirtuals = true)
1890
    {
1891
        if (class_exists('Subsite')) {
1892
            $origDisableSubsiteFilter = Subsite::$disable_subsite_filter;
1893
            Subsite::disable_subsite_filter(true);
1894
        }
1895
1896
        // Content links
1897
        $items = new ArrayList();
1898
1899
        // We merge all into a regular SS_List, because DataList doesn't support merge
1900
        if ($contentLinks = $this->BackLinkTracking()) {
0 ignored issues
show
Documentation Bug introduced by
The method BackLinkTracking does not exist on object<SilverStripe\CMS\Model\SiteTree>? Since you implemented __call, maybe consider adding a @method annotation.

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

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

class ParentClass {
    private $data = array();

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

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

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
1901
            $linkList = new ArrayList();
1902
            foreach ($contentLinks as $item) {
1903
                $item->DependentLinkType = 'Content link';
1904
                $linkList->push($item);
1905
            }
1906
            $items->merge($linkList);
1907
        }
1908
1909
        // Virtual pages
1910
        if ($includeVirtuals) {
1911
            $virtuals = $this->VirtualPages();
1912
            if ($virtuals) {
1913
                $virtualList = new ArrayList();
1914
                foreach ($virtuals as $item) {
1915
                    $item->DependentLinkType = 'Virtual page';
1916
                    $virtualList->push($item);
1917
                }
1918
                $items->merge($virtualList);
1919
            }
1920
        }
1921
1922
        // Redirector pages
1923
        $redirectors = RedirectorPage::get()->where(array(
1924
            '"RedirectorPage"."RedirectionType"' => 'Internal',
1925
            '"RedirectorPage"."LinkToID"' => $this->ID
1926
        ));
1927
        if ($redirectors) {
1928
            $redirectorList = new ArrayList();
1929
            foreach ($redirectors as $item) {
1930
                $item->DependentLinkType = 'Redirector page';
1931
                $redirectorList->push($item);
1932
            }
1933
            $items->merge($redirectorList);
1934
        }
1935
1936
        if (class_exists('Subsite')) {
1937
            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...
1938
        }
1939
1940
        return $items;
1941
    }
1942
1943
    /**
1944
     * Return all virtual pages that link to this page.
1945
     *
1946
     * @return DataList
1947
     */
1948
    public function VirtualPages()
1949
    {
1950
        $pages = parent::VirtualPages();
0 ignored issues
show
Bug introduced by
It seems like you code against a specific sub-type and not the parent class SilverStripe\ORM\DataObject as the method VirtualPages() does only exist in the following sub-classes of SilverStripe\ORM\DataObject: SilverStripe\CMS\Model\SiteTree. Maybe you want to instanceof check for one of these explicitly?

Let’s take a look at an example:

abstract class User
{
    /** @return string */
    abstract public function getPassword();
}

class MyUser extends User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different sub-classes of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the parent class:

    abstract class User
    {
        /** @return string */
        abstract public function getPassword();
    
        /** @return string */
        abstract public function getDisplayName();
    }
    
Loading history...
1951
1952
        // Disable subsite filter for these pages
1953
        if ($pages instanceof DataList) {
1954
            return $pages->setDataQueryParam('Subsite.filter', false);
1955
        } else {
1956
            return $pages;
1957
        }
1958
    }
1959
1960
    /**
1961
     * Returns a FieldList with which to create the main editing form.
1962
     *
1963
     * You can override this in your child classes to add extra fields - first get the parent fields using
1964
     * parent::getCMSFields(), then use addFieldToTab() on the FieldList.
1965
     *
1966
     * See {@link getSettingsFields()} for a different set of fields concerned with configuration aspects on the record,
1967
     * e.g. access control.
1968
     *
1969
     * @return FieldList The fields to be displayed in the CMS
1970
     */
1971
    public function getCMSFields()
1972
    {
1973
        // Status / message
1974
        // Create a status message for multiple parents
1975
        if ($this->ID && is_numeric($this->ID)) {
1976
            $linkedPages = $this->VirtualPages();
1977
1978
            $parentPageLinks = array();
1979
1980
            if ($linkedPages->count() > 0) {
1981
                /** @var VirtualPage $linkedPage */
1982
                foreach ($linkedPages as $linkedPage) {
1983
                    $parentPage = $linkedPage->Parent();
1984
                    if ($parentPage && $parentPage->exists()) {
1985
                        $link = Convert::raw2att($parentPage->CMSEditLink());
1986
                        $title = Convert::raw2xml($parentPage->Title);
1987
                    } else {
1988
                        $link = CMSPageEditController::singleton()->Link('show');
1989
                        $title = _t('SiteTree.TOPLEVEL', 'Site Content (Top Level)');
1990
                    }
1991
                    $parentPageLinks[] = "<a class=\"cmsEditlink\" href=\"{$link}\">{$title}</a>";
1992
                }
1993
1994
                $lastParent = array_pop($parentPageLinks);
1995
                $parentList = "'$lastParent'";
1996
1997
                if (count($parentPageLinks)) {
1998
                    $parentList = "'" . implode("', '", $parentPageLinks) . "' and "
1999
                        . $parentList;
2000
                }
2001
2002
                $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...
2003
                    'SiteTree.APPEARSVIRTUALPAGES',
2004
                    "This content also appears on the virtual pages in the {title} sections.",
2005
                    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...
2006
                );
2007
            }
2008
        }
2009
2010
        if ($this->HasBrokenLink || $this->HasBrokenFile) {
0 ignored issues
show
Documentation introduced by
The property HasBrokenLink does not exist on object<SilverStripe\CMS\Model\SiteTree>. Since you implemented __get, maybe consider adding a @property annotation.

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

<?php

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

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

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

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

}

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

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

See also the PhpDoc documentation for @property.

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

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

<?php

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

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

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

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

}

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

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

See also the PhpDoc documentation for @property.

Loading history...
2011
            $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...
2012
        }
2013
2014
        $dependentNote = '';
2015
        $dependentTable = new LiteralField('DependentNote', '<p></p>');
2016
2017
        // Create a table for showing pages linked to this one
2018
        $dependentPages = $this->DependentPages();
2019
        $dependentPagesCount = $dependentPages->count();
2020
        if ($dependentPagesCount) {
2021
            $dependentColumns = array(
2022
                'Title' => $this->fieldLabel('Title'),
2023
                'AbsoluteLink' => _t('SiteTree.DependtPageColumnURL', 'URL'),
2024
                'DependentLinkType' => _t('SiteTree.DependtPageColumnLinkType', 'Link type'),
2025
            );
2026
            if (class_exists('Subsite')) {
2027
                $dependentColumns['Subsite.Title'] = singleton('Subsite')->i18n_singular_name();
2028
            }
2029
2030
            $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>');
2031
            $dependentTable = GridField::create(
2032
                'DependentPages',
2033
                false,
2034
                $dependentPages
2035
            );
2036
            /** @var GridFieldDataColumns $dataColumns */
2037
            $dataColumns = $dependentTable->getConfig()->getComponentByType('SilverStripe\\Forms\\GridField\\GridFieldDataColumns');
2038
            $dataColumns
2039
                ->setDisplayFields($dependentColumns)
2040
                ->setFieldFormatting(array(
2041
                    'Title' => function ($value, &$item) {
2042
                        return sprintf(
2043
                            '<a href="admin/pages/edit/show/%d">%s</a>',
2044
                            (int)$item->ID,
2045
                            Convert::raw2xml($item->Title)
2046
                        );
2047
                    },
2048
                    '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...
2049
                        return sprintf(
2050
                            '<a href="%s" target="_blank">%s</a>',
2051
                            Convert::raw2xml($value),
2052
                            Convert::raw2xml($value)
2053
                        );
2054
                    }
2055
                ));
2056
        }
2057
2058
        $baseLink = Controller::join_links(
2059
            Director::absoluteBaseURL(),
2060
            (self::config()->nested_urls && $this->ParentID ? $this->Parent()->RelativeLink(true) : null)
0 ignored issues
show
Documentation introduced by
The property ParentID does not exist on object<SilverStripe\CMS\Model\SiteTree>. Since you implemented __get, maybe consider adding a @property annotation.

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

<?php

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

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

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

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

}

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

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

See also the PhpDoc documentation for @property.

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

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

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

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

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

Loading history...
2070
            ? $this->fieldLabel('LinkChangeNote')
2071
            : '';
2072
        if (!Config::inst()->get('SilverStripe\\View\\Parsers\\URLSegmentFilter', 'default_allow_multibyte')) {
2073
            $helpText .= _t('SiteTreeURLSegmentField.HelpChars', ' Special characters are automatically converted or removed.');
2074
        }
2075
        $urlsegment->setHelpText($helpText);
2076
2077
        $fields = new FieldList(
2078
            $rootTab = new TabSet(
2079
                "Root",
2080
                $tabMain = new Tab(
2081
                    'Main',
2082
                    new TextField("Title", $this->fieldLabel('Title')),
2083
                    $urlsegment,
2084
                    new TextField("MenuTitle", $this->fieldLabel('MenuTitle')),
2085
                    $htmlField = new HTMLEditorField("Content", _t('SiteTree.HTMLEDITORTITLE', "Content", 'HTML editor title')),
2086
                    ToggleCompositeField::create(
2087
                        'Metadata',
2088
                        _t('SiteTree.MetadataToggle', 'Metadata'),
2089
                        array(
2090
                            $metaFieldDesc = new TextareaField("MetaDescription", $this->fieldLabel('MetaDescription')),
2091
                            $metaFieldExtra = new TextareaField("ExtraMeta", $this->fieldLabel('ExtraMeta'))
2092
                        )
2093
                    )->setHeadingLevel(4)
2094
                ),
2095
                $tabDependent = new Tab(
2096
                    'Dependent',
2097
                    $dependentNote,
2098
                    $dependentTable
2099
                )
2100
            )
2101
        );
2102
        $htmlField->addExtraClass('stacked');
2103
2104
        // Help text for MetaData on page content editor
2105
        $metaFieldDesc
2106
            ->setRightTitle(
2107
                _t(
2108
                    'SiteTree.METADESCHELP',
2109
                    "Search engines use this content for displaying search results (although it will not influence their ranking)."
2110
                )
2111
            )
2112
            ->addExtraClass('help');
2113
        $metaFieldExtra
2114
            ->setRightTitle(
2115
                _t(
2116
                    'SiteTree.METAEXTRAHELP',
2117
                    "HTML tags for additional meta information. For example &lt;meta name=\"customName\" content=\"your custom content here\" /&gt;"
2118
                )
2119
            )
2120
            ->addExtraClass('help');
2121
2122
        // Conditional dependent pages tab
2123
        if ($dependentPagesCount) {
2124
            $tabDependent->setTitle(_t('SiteTree.TABDEPENDENT', "Dependent pages") . " ($dependentPagesCount)");
2125
        } else {
2126
            $fields->removeFieldFromTab('Root', 'Dependent');
2127
        }
2128
2129
        $tabMain->setTitle(_t('SiteTree.TABCONTENT', "Main Content"));
2130
2131
        if ($this->ObsoleteClassName) {
0 ignored issues
show
Bug introduced by
The property ObsoleteClassName does not seem to exist. Did you mean ClassName?

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

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

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

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

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

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

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
2136
            );
2137
2138
            $fields->addFieldToTab(
2139
                "Root.Main",
2140
                new LiteralField("ObsoleteWarningHeader", "<p class=\"message warning\">$obsoleteWarning</p>"),
2141
                "Title"
2142
            );
2143
        }
2144
2145
        if (file_exists(BASE_PATH . '/install.php')) {
2146
            $fields->addFieldToTab("Root.Main", new LiteralField(
2147
                "InstallWarningHeader",
2148
                "<p class=\"message warning\">" . _t(
2149
                    "SiteTree.REMOVE_INSTALL_WARNING",
2150
                    "Warning: You should remove install.php from this SilverStripe install for security reasons."
2151
                )
2152
                . "</p>"
2153
            ), "Title");
2154
        }
2155
2156
        if (self::$runCMSFieldsExtensions) {
2157
            $this->extend('updateCMSFields', $fields);
2158
        }
2159
2160
        return $fields;
2161
    }
2162
2163
2164
    /**
2165
     * Returns fields related to configuration aspects on this record, e.g. access control. See {@link getCMSFields()}
2166
     * for content-related fields.
2167
     *
2168
     * @return FieldList
2169
     */
2170
    public function getSettingsFields()
2171
    {
2172
        $groupsMap = array();
2173
        foreach (Group::get() as $group) {
2174
            // Listboxfield values are escaped, use ASCII char instead of &raquo;
2175
            $groupsMap[$group->ID] = $group->getBreadcrumbs(' > ');
2176
        }
2177
        asort($groupsMap);
2178
2179
        $fields = new FieldList(
2180
            $rootTab = new TabSet(
2181
                "Root",
2182
                $tabBehaviour = new Tab(
2183
                    'Settings',
2184
                    new DropdownField(
2185
                        "ClassName",
2186
                        $this->fieldLabel('ClassName'),
2187
                        $this->getClassDropdown()
2188
                    ),
2189
                    $parentTypeSelector = new CompositeField(
2190
                        $parentType = new OptionsetField("ParentType", _t("SiteTree.PAGELOCATION", "Page location"), array(
2191
                            "root" => _t("SiteTree.PARENTTYPE_ROOT", "Top-level page"),
2192
                            "subpage" => _t("SiteTree.PARENTTYPE_SUBPAGE", "Sub-page underneath a parent page"),
2193
                        )),
2194
                        $parentIDField = new TreeDropdownField("ParentID", $this->fieldLabel('ParentID'), self::class, 'ID', 'MenuTitle')
2195
                    ),
2196
                    $visibility = new FieldGroup(
2197
                        new CheckboxField("ShowInMenus", $this->fieldLabel('ShowInMenus')),
2198
                        new CheckboxField("ShowInSearch", $this->fieldLabel('ShowInSearch'))
2199
                    ),
2200
                    $viewersOptionsField = new OptionsetField(
2201
                        "CanViewType",
2202
                        _t('SiteTree.ACCESSHEADER', "Who can view this page?")
2203
                    ),
2204
                    $viewerGroupsField = ListboxField::create("ViewerGroups", _t('SiteTree.VIEWERGROUPS', "Viewer Groups"))
2205
                        ->setSource($groupsMap)
2206
                        ->setAttribute(
2207
                            'data-placeholder',
2208
                            _t('SiteTree.GroupPlaceholder', 'Click to select group')
2209
                        ),
2210
                    $editorsOptionsField = new OptionsetField(
2211
                        "CanEditType",
2212
                        _t('SiteTree.EDITHEADER', "Who can edit this page?")
2213
                    ),
2214
                    $editorGroupsField = ListboxField::create("EditorGroups", _t('SiteTree.EDITORGROUPS', "Editor Groups"))
2215
                        ->setSource($groupsMap)
2216
                        ->setAttribute(
2217
                            'data-placeholder',
2218
                            _t('SiteTree.GroupPlaceholder', 'Click to select group')
2219
                        )
2220
                )
2221
            )
2222
        );
2223
2224
        $parentType->addExtraClass('noborder');
2225
        $visibility->setTitle($this->fieldLabel('Visibility'));
2226
2227
2228
        // This filter ensures that the ParentID dropdown selection does not show this node,
2229
        // or its descendents, as this causes vanishing bugs
2230
        $parentIDField->setFilterFunction(create_function('$node', "return \$node->ID != {$this->ID};"));
2231
        $parentTypeSelector->addExtraClass('parentTypeSelector');
2232
2233
        $tabBehaviour->setTitle(_t('SiteTree.TABBEHAVIOUR', "Behavior"));
2234
2235
        // Make page location fields read-only if the user doesn't have the appropriate permission
2236
        if (!Permission::check("SITETREE_REORGANISE")) {
2237
            $fields->makeFieldReadonly('ParentType');
2238
            if ($this->getParentType() === 'root') {
2239
                $fields->removeByName('ParentID');
2240
            } else {
2241
                $fields->makeFieldReadonly('ParentID');
2242
            }
2243
        }
2244
2245
        $viewersOptionsSource = array();
2246
        $viewersOptionsSource["Inherit"] = _t('SiteTree.INHERIT', "Inherit from parent page");
2247
        $viewersOptionsSource["Anyone"] = _t('SiteTree.ACCESSANYONE', "Anyone");
2248
        $viewersOptionsSource["LoggedInUsers"] = _t('SiteTree.ACCESSLOGGEDIN', "Logged-in users");
2249
        $viewersOptionsSource["OnlyTheseUsers"] = _t('SiteTree.ACCESSONLYTHESE', "Only these people (choose from list)");
2250
        $viewersOptionsField->setSource($viewersOptionsSource);
2251
2252
        $editorsOptionsSource = array();
2253
        $editorsOptionsSource["Inherit"] = _t('SiteTree.INHERIT', "Inherit from parent page");
2254
        $editorsOptionsSource["LoggedInUsers"] = _t('SiteTree.EDITANYONE', "Anyone who can log-in to the CMS");
2255
        $editorsOptionsSource["OnlyTheseUsers"] = _t('SiteTree.EDITONLYTHESE', "Only these people (choose from list)");
2256
        $editorsOptionsField->setSource($editorsOptionsSource);
2257
2258
        if (!Permission::check('SITETREE_GRANT_ACCESS')) {
2259
            $fields->makeFieldReadonly($viewersOptionsField);
2260
            if ($this->CanViewType == 'OnlyTheseUsers') {
2261
                $fields->makeFieldReadonly($viewerGroupsField);
2262
            } else {
2263
                $fields->removeByName('ViewerGroups');
2264
            }
2265
2266
            $fields->makeFieldReadonly($editorsOptionsField);
2267
            if ($this->CanEditType == 'OnlyTheseUsers') {
2268
                $fields->makeFieldReadonly($editorGroupsField);
2269
            } else {
2270
                $fields->removeByName('EditorGroups');
2271
            }
2272
        }
2273
2274
        if (self::$runCMSFieldsExtensions) {
2275
            $this->extend('updateSettingsFields', $fields);
2276
        }
2277
2278
        return $fields;
2279
    }
2280
2281
    /**
2282
     * @param bool $includerelations A boolean value to indicate if the labels returned should include relation fields
2283
     * @return array
2284
     */
2285
    public function fieldLabels($includerelations = true)
2286
    {
2287
        $cacheKey = static::class . '_' . $includerelations;
2288
        if (!isset(self::$_cache_field_labels[$cacheKey])) {
2289
            $labels = parent::fieldLabels($includerelations);
2290
            $labels['Title'] = _t('SiteTree.PAGETITLE', "Page name");
2291
            $labels['MenuTitle'] = _t('SiteTree.MENUTITLE', "Navigation label");
2292
            $labels['MetaDescription'] = _t('SiteTree.METADESC', "Meta Description");
2293
            $labels['ExtraMeta'] = _t('SiteTree.METAEXTRA', "Custom Meta Tags");
2294
            $labels['ClassName'] = _t('SiteTree.PAGETYPE', "Page type", 'Classname of a page object');
2295
            $labels['ParentType'] = _t('SiteTree.PARENTTYPE', "Page location");
2296
            $labels['ParentID'] = _t('SiteTree.PARENTID', "Parent page");
2297
            $labels['ShowInMenus'] =_t('SiteTree.SHOWINMENUS', "Show in menus?");
2298
            $labels['ShowInSearch'] = _t('SiteTree.SHOWINSEARCH', "Show in search?");
2299
            $labels['ProvideComments'] = _t('SiteTree.ALLOWCOMMENTS', "Allow comments on this page?");
2300
            $labels['ViewerGroups'] = _t('SiteTree.VIEWERGROUPS', "Viewer Groups");
2301
            $labels['EditorGroups'] = _t('SiteTree.EDITORGROUPS', "Editor Groups");
2302
            $labels['URLSegment'] = _t('SiteTree.URLSegment', 'URL Segment', 'URL for this page');
2303
            $labels['Content'] = _t('SiteTree.Content', 'Content', 'Main HTML Content for a page');
2304
            $labels['CanViewType'] = _t('SiteTree.Viewers', 'Viewers Groups');
2305
            $labels['CanEditType'] = _t('SiteTree.Editors', 'Editors Groups');
2306
            $labels['Comments'] = _t('SiteTree.Comments', 'Comments');
2307
            $labels['Visibility'] = _t('SiteTree.Visibility', 'Visibility');
2308
            $labels['LinkChangeNote'] = _t(
2309
                'SiteTree.LINKCHANGENOTE',
2310
                'Changing this page\'s link will also affect the links of all child pages.'
2311
            );
2312
2313
            if ($includerelations) {
2314
                $labels['Parent'] = _t('SiteTree.has_one_Parent', 'Parent Page', 'The parent page in the site hierarchy');
2315
                $labels['LinkTracking'] = _t('SiteTree.many_many_LinkTracking', 'Link Tracking');
2316
                $labels['ImageTracking'] = _t('SiteTree.many_many_ImageTracking', 'Image Tracking');
2317
                $labels['BackLinkTracking'] = _t('SiteTree.many_many_BackLinkTracking', 'Backlink Tracking');
2318
            }
2319
2320
            self::$_cache_field_labels[$cacheKey] = $labels;
2321
        }
2322
2323
        return self::$_cache_field_labels[$cacheKey];
2324
    }
2325
2326
    /**
2327
     * Get the actions available in the CMS for this page - eg Save, Publish.
2328
     *
2329
     * Frontend scripts and styles know how to handle the following FormFields:
2330
     * - top-level FormActions appear as standalone buttons
2331
     * - top-level CompositeField with FormActions within appear as grouped buttons
2332
     * - TabSet & Tabs appear as a drop ups
2333
     * - FormActions within the Tab are restyled as links
2334
     * - major actions can provide alternate states for richer presentation (see ssui.button widget extension)
2335
     *
2336
     * @return FieldList The available actions for this page.
2337
     */
2338
    public function getCMSActions()
2339
    {
2340
        // Get status of page
2341
        $isOnDraft = $this->isOnDraft();
0 ignored issues
show
Documentation Bug introduced by
The method isOnDraft does not exist on object<SilverStripe\CMS\Model\SiteTree>? Since you implemented __call, maybe consider adding a @method annotation.

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

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

class ParentClass {
    private $data = array();

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

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

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

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

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

class ParentClass {
    private $data = array();

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

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

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

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

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

class ParentClass {
    private $data = array();

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

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

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

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

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

class ParentClass {
    private $data = array();

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

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

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
2348
        $canEdit = $this->canEdit();
2349
2350
        // Major actions appear as buttons immediately visible as page actions.
2351
        $majorActions = CompositeField::create()->setName('MajorActions');
2352
        $majorActions->setFieldHolderTemplate(get_class($majorActions) . '_holder_buttongroup');
2353
2354
        // Minor options are hidden behind a drop-up and appear as links (although they are still FormActions).
2355
        $rootTabSet = new TabSet('ActionMenus');
2356
        $moreOptions = new Tab(
2357
            'MoreOptions',
2358
            _t('SiteTree.MoreOptions', 'More options', 'Expands a view for more buttons')
2359
        );
2360
        $rootTabSet->push($moreOptions);
2361
        $rootTabSet->addExtraClass('ss-ui-action-tabset action-menus noborder');
2362
2363
        // Render page information into the "more-options" drop-up, on the top.
2364
        $liveRecord = Versioned::get_by_stage(self::class, Versioned::LIVE)->byID($this->ID);
2365
        $infoTemplate = SSViewer::get_templates_by_class(static::class, '_Information', self::class);
2366
        $moreOptions->push(
2367
            new LiteralField(
2368
                'Information',
2369
                $this->customise(array(
2370
                    'Live' => $liveRecord,
2371
                    'ExistsOnLive' => $isPublished
2372
                ))->renderWith($infoTemplate)
2373
            )
2374
        );
2375
2376
        // Add to campaign option if not-archived and has publish permission
2377
        if (($isPublished || $isOnDraft) && $canPublish) {
2378
            $moreOptions->push(
2379
                AddToCampaignHandler_FormAction::create()
2380
                    ->removeExtraClass('btn-primary')
2381
                    ->addExtraClass('btn-secondary')
2382
            );
2383
        }
2384
2385
        // "readonly"/viewing version that isn't the current version of the record
2386
        $stageRecord = Versioned::get_by_stage(static::class, Versioned::DRAFT)->byID($this->ID);
2387
        /** @skipUpgrade */
2388
        if ($stageRecord && $stageRecord->Version != $this->Version) {
0 ignored issues
show
Bug introduced by
The property Version does not seem to exist. Did you mean versioning?

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

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

Loading history...
2389
            $moreOptions->push(FormAction::create('email', _t('CMSMain.EMAIL', 'Email')));
2390
            $moreOptions->push(FormAction::create('rollback', _t('CMSMain.ROLLBACK', 'Roll back to this version')));
2391
            $actions = new FieldList(array($majorActions, $rootTabSet));
2392
2393
            // getCMSActions() can be extended with updateCMSActions() on a extension
2394
            $this->extend('updateCMSActions', $actions);
2395
            return $actions;
2396
        }
2397
2398
        // "unpublish"
2399
        if ($isPublished && $canPublish && $isOnDraft && $canUnpublish) {
2400
            $moreOptions->push(
2401
                FormAction::create('unpublish', _t('SiteTree.BUTTONUNPUBLISH', 'Unpublish'), 'delete')
2402
                    ->setDescription(_t('SiteTree.BUTTONUNPUBLISHDESC', 'Remove this page from the published site'))
2403
                    ->addExtraClass('btn-secondary')
2404
            );
2405
        }
2406
2407
        // "rollback"
2408
        if ($isOnDraft && $isPublished && $canEdit && $stagesDiffer) {
2409
            $moreOptions->push(
2410
                FormAction::create('rollback', _t('SiteTree.BUTTONCANCELDRAFT', 'Cancel draft changes'))
2411
                    ->setDescription(_t(
2412
                        'SiteTree.BUTTONCANCELDRAFTDESC',
2413
                        'Delete your draft and revert to the currently published page'
2414
                    ))
2415
                    ->addExtraClass('btn-secondary')
2416
            );
2417
        }
2418
2419
        // "restore"
2420
        if ($canEdit && !$isOnDraft && $isPublished) {
2421
            $majorActions->push(FormAction::create('revert', _t('CMSMain.RESTORE', 'Restore')));
2422
        }
2423
2424
        // Check if we can restore a deleted page
2425
        // Note: It would be nice to have a canRestore() permission at some point
2426
        if ($canEdit && !$isOnDraft && !$isPublished) {
2427
            // Determine if we should force a restore to root (where once it was a subpage)
2428
            $restoreToRoot = $this->isParentArchived();
2429
2430
            // "restore"
2431
            $title = $restoreToRoot
2432
                ? _t('CMSMain.RESTORE_TO_ROOT', 'Restore draft at top level')
2433
                : _t('CMSMain.RESTORE', 'Restore draft');
2434
            $description = $restoreToRoot
2435
                ? _t('CMSMain.RESTORE_TO_ROOT_DESC', 'Restore the archived version to draft as a top level page')
2436
                : _t('CMSMain.RESTORE_DESC', 'Restore the archived version to draft');
2437
            $majorActions->push(
2438
                FormAction::create('restore', $title)
2439
                    ->setDescription($description)
2440
                    ->setAttribute('data-to-root', $restoreToRoot)
0 ignored issues
show
Documentation introduced by
$restoreToRoot is of type boolean, but the function expects a string.

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
2441
                    ->setAttribute('data-icon', 'decline')
2442
            );
2443
        }
2444
2445
        // If a page is on any stage it can be archived
2446
        if (($isOnDraft || $isPublished) && $this->canArchive()) {
0 ignored issues
show
Documentation Bug introduced by
The method canArchive does not exist on object<SilverStripe\CMS\Model\SiteTree>? Since you implemented __call, maybe consider adding a @method annotation.

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

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

class ParentClass {
    private $data = array();

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

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

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
2447
            $title = $isPublished
2448
                ? _t('CMSMain.UNPUBLISH_AND_ARCHIVE', 'Unpublish and archive')
2449
                : _t('CMSMain.ARCHIVE', 'Archive');
2450
            $moreOptions->push(
2451
                FormAction::create('archive', $title)
2452
                    ->addExtraClass('delete btn btn-secondary')
2453
                    ->setDescription(_t(
2454
                        'SiteTree.BUTTONDELETEDESC',
2455
                        'Remove from draft/live and send to archive'
2456
                    ))
2457
            );
2458
        }
2459
2460
        // "save", supports an alternate state that is still clickable, but notifies the user that the action is not needed.
2461
        if ($canEdit && $isOnDraft) {
2462
            $majorActions->push(
2463
                FormAction::create('save', _t('SiteTree.BUTTONSAVED', 'Saved'))
2464
                    ->addExtraClass('btn-secondary-outline font-icon-check-mark')
2465
                    ->setAttribute('data-btn-alternate', 'btn action btn-primary font-icon-save')
2466
                    ->setUseButtonTag(true)
2467
                    ->setAttribute('data-text-alternate', _t('CMSMain.SAVEDRAFT', 'Save draft'))
2468
            );
2469
        }
2470
2471
        if ($canPublish && $isOnDraft) {
2472
            // "publish", as with "save", it supports an alternate state to show when action is needed.
2473
            $majorActions->push(
2474
                $publish = FormAction::create('publish', _t('SiteTree.BUTTONPUBLISHED', 'Published'))
2475
                    ->addExtraClass('btn-secondary-outline font-icon-check-mark')
2476
                    ->setAttribute('data-btn-alternate', 'btn action btn-primary font-icon-rocket')
2477
                    ->setUseButtonTag(true)
2478
                    ->setAttribute('data-text-alternate', _t('SiteTree.BUTTONSAVEPUBLISH', 'Save & publish'))
2479
            );
2480
2481
            // Set up the initial state of the button to reflect the state of the underlying SiteTree object.
2482
            if ($stagesDiffer) {
2483
                $publish->addExtraClass('btn-primary font-icon-rocket');
2484
                $publish->setTitle(_t('SiteTree.BUTTONSAVEPUBLISH', 'Save & publish'));
2485
                $publish->removeExtraClass('btn-secondary-outline font-icon-check-mark');
2486
            }
2487
        }
2488
2489
        $actions = new FieldList(array($majorActions, $rootTabSet));
2490
2491
        // Hook for extensions to add/remove actions.
2492
        $this->extend('updateCMSActions', $actions);
2493
2494
        return $actions;
2495
    }
2496
2497
    public function onAfterPublish()
2498
    {
2499
        // Force live sort order to match stage sort order
2500
        DB::prepared_query(
2501
            'UPDATE "SiteTree_Live"
2502
			SET "Sort" = (SELECT "SiteTree"."Sort" FROM "SiteTree" WHERE "SiteTree_Live"."ID" = "SiteTree"."ID")
2503
			WHERE EXISTS (SELECT "SiteTree"."Sort" FROM "SiteTree" WHERE "SiteTree_Live"."ID" = "SiteTree"."ID") AND "ParentID" = ?',
2504
            array($this->ParentID)
0 ignored issues
show
Documentation introduced by
The property ParentID does not exist on object<SilverStripe\CMS\Model\SiteTree>. Since you implemented __get, maybe consider adding a @property annotation.

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

<?php

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

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

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

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

}

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

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

See also the PhpDoc documentation for @property.

Loading history...
2505
        );
2506
    }
2507
2508
    /**
2509
     * Update draft dependant pages
2510
     */
2511
    public function onAfterRevertToLive()
2512
    {
2513
        // Use an alias to get the updates made by $this->publish
2514
        /** @var SiteTree $stageSelf */
2515
        $stageSelf = Versioned::get_by_stage(self::class, Versioned::DRAFT)->byID($this->ID);
2516
        $stageSelf->writeWithoutVersion();
0 ignored issues
show
Documentation Bug introduced by
The method writeWithoutVersion does not exist on object<SilverStripe\CMS\Model\SiteTree>? Since you implemented __call, maybe consider adding a @method annotation.

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

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

class ParentClass {
    private $data = array();

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

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

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

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

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

class ParentClass {
    private $data = array();

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

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

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
2522
        }
2523
    }
2524
2525
    /**
2526
     * Determine if this page references a parent which is archived, and not available in stage
2527
     *
2528
     * @return bool True if there is an archived parent
2529
     */
2530
    protected function isParentArchived()
2531
    {
2532
        if ($parentID = $this->ParentID) {
0 ignored issues
show
Documentation introduced by
The property ParentID does not exist on object<SilverStripe\CMS\Model\SiteTree>. Since you implemented __set, maybe consider adding a @property annotation.

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

<?php

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

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

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

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

}

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

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

See also the PhpDoc documentation for @property.

Loading history...
2533
            /** @var SiteTree $parentPage */
2534
            $parentPage = Versioned::get_latest_version(self::class, $parentID);
2535
            if (!$parentPage || !$parentPage->isOnDraft()) {
0 ignored issues
show
Documentation Bug introduced by
The method isOnDraft does not exist on object<SilverStripe\CMS\Model\SiteTree>? Since you implemented __call, maybe consider adding a @method annotation.

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

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

class ParentClass {
    private $data = array();

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

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

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
2536
                return true;
2537
            }
2538
        }
2539
        return false;
2540
    }
2541
2542
    /**
2543
     * Restore the content in the active copy of this SiteTree page to the stage site.
2544
     *
2545
     * @return self
2546
     */
2547
    public function doRestoreToStage()
2548
    {
2549
        $this->invokeWithExtensions('onBeforeRestoreToStage', $this);
2550
2551
        // Ensure that the parent page is restored, otherwise restore to root
2552
        if ($this->isParentArchived()) {
2553
            $this->ParentID = 0;
0 ignored issues
show
Documentation introduced by
The property ParentID does not exist on object<SilverStripe\CMS\Model\SiteTree>. Since you implemented __set, maybe consider adding a @property annotation.

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

<?php

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

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

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

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

}

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

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

See also the PhpDoc documentation for @property.

Loading history...
2554
        }
2555
2556
        // if no record can be found on draft stage (meaning it has been "deleted from draft" before),
2557
        // create an empty record
2558
        if (!DB::prepared_query("SELECT \"ID\" FROM \"SiteTree\" WHERE \"ID\" = ?", array($this->ID))->value()) {
2559
            $conn = DB::get_conn();
2560
            if (method_exists($conn, 'allowPrimaryKeyEditing')) {
2561
                $conn->allowPrimaryKeyEditing(self::class, true);
0 ignored issues
show
Bug introduced by
The method allowPrimaryKeyEditing() does not seem to exist on object<SilverStripe\ORM\Connect\Database>.

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

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

Loading history...
2562
            }
2563
            DB::prepared_query("INSERT INTO \"SiteTree\" (\"ID\") VALUES (?)", array($this->ID));
2564
            if (method_exists($conn, 'allowPrimaryKeyEditing')) {
2565
                $conn->allowPrimaryKeyEditing(self::class, false);
0 ignored issues
show
Bug introduced by
The method allowPrimaryKeyEditing() does not seem to exist on object<SilverStripe\ORM\Connect\Database>.

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

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

Loading history...
2566
            }
2567
        }
2568
2569
        $oldReadingMode = Versioned::get_reading_mode();
2570
        Versioned::set_stage(Versioned::DRAFT);
2571
        $this->forceChange();
2572
        $this->write();
2573
2574
        /** @var SiteTree $result */
2575
        $result = DataObject::get_by_id(self::class, $this->ID);
2576
2577
        // Need to update pages linking to this one as no longer broken
2578
        foreach ($result->DependentPages(false) as $page) {
2579
            // $page->write() calls syncLinkTracking, which does all the hard work for us.
2580
            $page->write();
2581
        }
2582
2583
        Versioned::set_reading_mode($oldReadingMode);
2584
2585
        $this->invokeWithExtensions('onAfterRestoreToStage', $this);
2586
2587
        return $result;
2588
    }
2589
2590
    /**
2591
     * Check if this page is new - that is, if it has yet to have been written to the database.
2592
     *
2593
     * @return bool
2594
     */
2595
    public function isNew()
2596
    {
2597
        /**
2598
         * This check was a problem for a self-hosted site, and may indicate a bug in the interpreter on their server,
2599
         * or a bug here. Changing the condition from empty($this->ID) to !$this->ID && !$this->record['ID'] fixed this.
2600
         */
2601
        if (empty($this->ID)) {
2602
            return true;
2603
        }
2604
2605
        if (is_numeric($this->ID)) {
2606
            return false;
2607
        }
2608
2609
        return stripos($this->ID, 'new') === 0;
2610
    }
2611
2612
    /**
2613
     * Get the class dropdown used in the CMS to change the class of a page. This returns the list of options in the
2614
     * dropdown as a Map from class name to singular name. Filters by {@link SiteTree->canCreate()}, as well as
2615
     * {@link SiteTree::$needs_permission}.
2616
     *
2617
     * @return array
2618
     */
2619
    protected function getClassDropdown()
2620
    {
2621
        $classes = self::page_type_classes();
2622
        $currentClass = null;
2623
2624
        $result = array();
2625
        foreach ($classes as $class) {
2626
            $instance = singleton($class);
2627
2628
            // if the current page type is this the same as the class type always show the page type in the list
2629
            if ($this->ClassName != $instance->ClassName) {
2630
                if ($instance instanceof HiddenClass) {
2631
                    continue;
2632
                }
2633
                if (!$instance->canCreate(null, array('Parent' => $this->ParentID ? $this->Parent() : null))) {
0 ignored issues
show
Documentation introduced by
The property ParentID does not exist on object<SilverStripe\CMS\Model\SiteTree>. Since you implemented __get, maybe consider adding a @property annotation.

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

<?php

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

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

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

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

}

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

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

See also the PhpDoc documentation for @property.

Loading history...
2634
                    continue;
2635
                }
2636
            }
2637
2638
            if ($perms = $instance->stat('need_permission')) {
2639
                if (!$this->can($perms)) {
2640
                    continue;
2641
                }
2642
            }
2643
2644
            $pageTypeName = $instance->i18n_singular_name();
2645
2646
            $currentClass = $class;
2647
            $result[$class] = $pageTypeName;
2648
2649
            // If we're in translation mode, the link between the translated pagetype title and the actual classname
2650
            // might not be obvious, so we add it in parantheses. Example: class "RedirectorPage" has the title
2651
            // "Weiterleitung" in German, so it shows up as "Weiterleitung (RedirectorPage)"
2652
            if (i18n::get_lang_from_locale(i18n::get_locale()) != 'en') {
2653
                $result[$class] = $result[$class] .  " ({$class})";
2654
            }
2655
        }
2656
2657
        // sort alphabetically, and put current on top
2658
        asort($result);
2659
        if ($currentClass) {
2660
            $currentPageTypeName = $result[$currentClass];
2661
            unset($result[$currentClass]);
2662
            $result = array_reverse($result);
2663
            $result[$currentClass] = $currentPageTypeName;
2664
            $result = array_reverse($result);
2665
        }
2666
2667
        return $result;
2668
    }
2669
2670
    /**
2671
     * Returns an array of the class names of classes that are allowed to be children of this class.
2672
     *
2673
     * @return string[]
2674
     */
2675
    public function allowedChildren()
2676
    {
2677
        $allowedChildren = array();
2678
        $candidates = $this->stat('allowed_children');
2679
        if ($candidates && $candidates != "none" && $candidates != "SiteTree_root") {
2680
            foreach ($candidates as $candidate) {
2681
                // If a classname is prefixed by "*", such as "*Page", then only that class is allowed - no subclasses.
2682
                // Otherwise, the class and all its subclasses are allowed.
2683
                if (substr($candidate, 0, 1) == '*') {
2684
                    $allowedChildren[] = substr($candidate, 1);
2685
                } elseif ($subclasses = ClassInfo::subclassesFor($candidate)) {
2686
                    foreach ($subclasses as $subclass) {
2687
                        if ($subclass == 'SiteTree_root' || singleton($subclass) instanceof HiddenClass) {
2688
                            continue;
2689
                        }
2690
                        $allowedChildren[] = $subclass;
2691
                    }
2692
                }
2693
            }
2694
        }
2695
2696
        return $allowedChildren;
2697
    }
2698
2699
    /**
2700
     * Returns the class name of the default class for children of this page.
2701
     *
2702
     * @return string
2703
     */
2704
    public function defaultChild()
2705
    {
2706
        $default = $this->stat('default_child');
2707
        $allowed = $this->allowedChildren();
2708
        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...
2709
            if (!$default || !in_array($default, $allowed)) {
2710
                $default = reset($allowed);
2711
            }
2712
            return $default;
2713
        }
2714
        return null;
2715
    }
2716
2717
    /**
2718
     * Returns the class name of the default class for the parent of this page.
2719
     *
2720
     * @return string
2721
     */
2722
    public function defaultParent()
2723
    {
2724
        return $this->stat('default_parent');
2725
    }
2726
2727
    /**
2728
     * Get the title for use in menus for this page. If the MenuTitle field is set it returns that, else it returns the
2729
     * Title field.
2730
     *
2731
     * @return string
2732
     */
2733
    public function getMenuTitle()
2734
    {
2735
        if ($value = $this->getField("MenuTitle")) {
2736
            return $value;
2737
        } else {
2738
            return $this->getField("Title");
2739
        }
2740
    }
2741
2742
2743
    /**
2744
     * Set the menu title for this page.
2745
     *
2746
     * @param string $value
2747
     */
2748
    public function setMenuTitle($value)
2749
    {
2750
        if ($value == $this->getField("Title")) {
2751
            $this->setField("MenuTitle", null);
2752
        } else {
2753
            $this->setField("MenuTitle", $value);
2754
        }
2755
    }
2756
2757
    /**
2758
     * A flag provides the user with additional data about the current page status, for example a "removed from draft"
2759
     * status. Each page can have more than one status flag. Returns a map of a unique key to a (localized) title for
2760
     * the flag. The unique key can be reused as a CSS class. Use the 'updateStatusFlags' extension point to customize
2761
     * the flags.
2762
     *
2763
     * Example (simple):
2764
     *   "deletedonlive" => "Deleted"
2765
     *
2766
     * Example (with optional title attribute):
2767
     *   "deletedonlive" => array('text' => "Deleted", 'title' => 'This page has been deleted')
2768
     *
2769
     * @param bool $cached Whether to serve the fields from cache; false regenerate them
2770
     * @return array
2771
     */
2772
    public function getStatusFlags($cached = true)
2773
    {
2774
        if (!$this->_cache_statusFlags || !$cached) {
2775
            $flags = array();
2776
            if ($this->isOnLiveOnly()) {
0 ignored issues
show
Documentation Bug introduced by
The method isOnLiveOnly does not exist on object<SilverStripe\CMS\Model\SiteTree>? Since you implemented __call, maybe consider adding a @method annotation.

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

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

class ParentClass {
    private $data = array();

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

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

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
2777
                $flags['removedfromdraft'] = array(
2778
                    'text' => _t('SiteTree.ONLIVEONLYSHORT', 'On live only'),
2779
                    'title' => _t('SiteTree.ONLIVEONLYSHORTHELP', 'Page is published, but has been deleted from draft'),
2780
                );
2781
            } elseif ($this->isArchived()) {
0 ignored issues
show
Documentation Bug introduced by
The method isArchived does not exist on object<SilverStripe\CMS\Model\SiteTree>? Since you implemented __call, maybe consider adding a @method annotation.

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

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

class ParentClass {
    private $data = array();

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

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

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
2782
                $flags['archived'] = array(
2783
                    'text' => _t('SiteTree.ARCHIVEDPAGESHORT', 'Archived'),
2784
                    'title' => _t('SiteTree.ARCHIVEDPAGEHELP', 'Page is removed from draft and live'),
2785
                );
2786
            } elseif ($this->isOnDraftOnly()) {
0 ignored issues
show
Documentation Bug introduced by
The method isOnDraftOnly does not exist on object<SilverStripe\CMS\Model\SiteTree>? Since you implemented __call, maybe consider adding a @method annotation.

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

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

class ParentClass {
    private $data = array();

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

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

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
2787
                $flags['addedtodraft'] = array(
2788
                    'text' => _t('SiteTree.ADDEDTODRAFTSHORT', 'Draft'),
2789
                    'title' => _t('SiteTree.ADDEDTODRAFTHELP', "Page has not been published yet")
2790
                );
2791
            } elseif ($this->isModifiedOnDraft()) {
0 ignored issues
show
Documentation Bug introduced by
The method isModifiedOnDraft does not exist on object<SilverStripe\CMS\Model\SiteTree>? Since you implemented __call, maybe consider adding a @method annotation.

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

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

class ParentClass {
    private $data = array();

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

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

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
2792
                $flags['modified'] = array(
2793
                    'text' => _t('SiteTree.MODIFIEDONDRAFTSHORT', 'Modified'),
2794
                    'title' => _t('SiteTree.MODIFIEDONDRAFTHELP', 'Page has unpublished changes'),
2795
                );
2796
            }
2797
2798
            $this->extend('updateStatusFlags', $flags);
2799
2800
            $this->_cache_statusFlags = $flags;
2801
        }
2802
2803
        return $this->_cache_statusFlags;
2804
    }
2805
2806
    /**
2807
     * getTreeTitle will return three <span> html DOM elements, an empty <span> with the class 'jstree-pageicon' in
2808
     * front, following by a <span> wrapping around its MenutTitle, then following by a <span> indicating its
2809
     * publication status.
2810
     *
2811
     * @return string An HTML string ready to be directly used in a template
2812
     */
2813
    public function getTreeTitle()
2814
    {
2815
        // Build the list of candidate children
2816
        $children = array();
2817
        $candidates = static::page_type_classes();
2818
        foreach ($this->allowedChildren() as $childClass) {
2819
            if (!in_array($childClass, $candidates)) {
2820
                continue;
2821
            }
2822
            $child = singleton($childClass);
2823
            if ($child->canCreate(null, array('Parent' => $this))) {
2824
                $children[$childClass] = $child->i18n_singular_name();
2825
            }
2826
        }
2827
        $flags = $this->getStatusFlags();
2828
        $treeTitle = sprintf(
2829
            "<span class=\"jstree-pageicon\"></span><span class=\"item\" data-allowedchildren=\"%s\">%s</span>",
2830
            Convert::raw2att(Convert::raw2json($children)),
2831
            Convert::raw2xml(str_replace(array("\n","\r"), "", $this->MenuTitle))
2832
        );
2833
        foreach ($flags as $class => $data) {
2834
            if (is_string($data)) {
2835
                $data = array('text' => $data);
2836
            }
2837
            $treeTitle .= sprintf(
2838
                "<span class=\"badge %s\"%s>%s</span>",
2839
                'status-' . Convert::raw2xml($class),
2840
                (isset($data['title'])) ? sprintf(' title="%s"', Convert::raw2xml($data['title'])) : '',
2841
                Convert::raw2xml($data['text'])
2842
            );
2843
        }
2844
2845
        return $treeTitle;
2846
    }
2847
2848
    /**
2849
     * Returns the page in the current page stack of the given level. Level(1) will return the main menu item that
2850
     * we're currently inside, etc.
2851
     *
2852
     * @param int $level
2853
     * @return SiteTree
2854
     */
2855
    public function Level($level)
2856
    {
2857
        $parent = $this;
2858
        $stack = array($parent);
2859
        while (($parent = $parent->Parent()) && $parent->exists()) {
2860
            array_unshift($stack, $parent);
2861
        }
2862
2863
        return isset($stack[$level-1]) ? $stack[$level-1] : null;
2864
    }
2865
2866
    /**
2867
     * Gets the depth of this page in the sitetree, where 1 is the root level
2868
     *
2869
     * @return int
2870
     */
2871
    public function getPageLevel()
2872
    {
2873
        if ($this->ParentID) {
0 ignored issues
show
Documentation introduced by
The property ParentID does not exist on object<SilverStripe\CMS\Model\SiteTree>. Since you implemented __get, maybe consider adding a @property annotation.

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

<?php

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

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

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

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

}

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

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

See also the PhpDoc documentation for @property.

Loading history...
2874
            return 1 + $this->Parent()->getPageLevel();
2875
        }
2876
        return 1;
2877
    }
2878
2879
    /**
2880
     * Find the controller name by our convention of {$ModelClass}Controller
2881
     *
2882
     * @return string
2883
     */
2884
    public function getControllerName()
2885
    {
2886
        //default controller for SiteTree objects
2887
        $controller = ContentController::class;
2888
2889
        //go through the ancestry for this class looking for
2890
        $ancestry = ClassInfo::ancestry(static::class);
2891
        // loop over the array going from the deepest descendant (ie: the current class) to SiteTree
2892
        while ($class = array_pop($ancestry)) {
2893
            //we don't need to go any deeper than the SiteTree class
2894
            if ($class == SiteTree::class) {
2895
                break;
2896
            }
2897
            // If we have a class of "{$ClassName}Controller" then we found our controller
2898
            if (class_exists($candidate = sprintf('%sController', $class))) {
2899
                $controller = $candidate;
2900
                break;
2901
            } elseif (class_exists($candidate = sprintf('%s_Controller', $class))) {
2902
                // Support the legacy underscored filename, but raise a deprecation notice
2903
                Deprecation::notice(
2904
                    '5.0',
2905
                    'Underscored controller class names are deprecated. Use "MyController" instead of "My_Controller".',
2906
                    Deprecation::SCOPE_GLOBAL
2907
                );
2908
                $controller = $candidate;
2909
                break;
2910
            }
2911
        }
2912
2913
        return $controller;
2914
    }
2915
2916
    /**
2917
     * Return the CSS classes to apply to this node in the CMS tree.
2918
     *
2919
     * @param string $numChildrenMethod
2920
     * @return string
2921
     */
2922
    public function CMSTreeClasses($numChildrenMethod = "numChildren")
2923
    {
2924
        $classes = sprintf('class-%s', static::class);
2925
        if ($this->HasBrokenFile || $this->HasBrokenLink) {
0 ignored issues
show
Documentation introduced by
The property HasBrokenFile does not exist on object<SilverStripe\CMS\Model\SiteTree>. Since you implemented __get, maybe consider adding a @property annotation.

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

<?php

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

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

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

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

}

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

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

See also the PhpDoc documentation for @property.

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

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

<?php

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

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

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

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

}

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

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

See also the PhpDoc documentation for @property.

Loading history...
2926
            $classes .= " BrokenLink";
2927
        }
2928
2929
        if (!$this->canAddChildren()) {
2930
            $classes .= " nochildren";
2931
        }
2932
2933
        if (!$this->canEdit() && !$this->canAddChildren()) {
2934
            if (!$this->canView()) {
2935
                $classes .= " disabled";
2936
            } else {
2937
                $classes .= " edit-disabled";
2938
            }
2939
        }
2940
2941
        if (!$this->ShowInMenus) {
2942
            $classes .= " notinmenu";
2943
        }
2944
2945
        //TODO: Add integration
2946
        /*
2947
		if($this->hasExtension('Translatable') && $controller->Locale != Translatable::default_locale() && !$this->isTranslation())
2948
			$classes .= " untranslated ";
2949
		*/
2950
        $classes .= $this->markingClasses($numChildrenMethod);
0 ignored issues
show
Documentation Bug introduced by
The method markingClasses does not exist on object<SilverStripe\CMS\Model\SiteTree>? Since you implemented __call, maybe consider adding a @method annotation.

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

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

class ParentClass {
    private $data = array();

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

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

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
2951
2952
        return $classes;
2953
    }
2954
2955
    /**
2956
     * Stops extendCMSFields() being called on getCMSFields(). This is useful when you need access to fields added by
2957
     * subclasses of SiteTree in a extension. Call before calling parent::getCMSFields(), and reenable afterwards.
2958
     */
2959
    public static function disableCMSFieldsExtensions()
2960
    {
2961
        self::$runCMSFieldsExtensions = false;
2962
    }
2963
2964
    /**
2965
     * Reenables extendCMSFields() being called on getCMSFields() after it has been disabled by
2966
     * disableCMSFieldsExtensions().
2967
     */
2968
    public static function enableCMSFieldsExtensions()
2969
    {
2970
        self::$runCMSFieldsExtensions = true;
2971
    }
2972
2973
    public function providePermissions()
2974
    {
2975
        return array(
2976
            'SITETREE_GRANT_ACCESS' => array(
2977
                'name' => _t('SiteTree.PERMISSION_GRANTACCESS_DESCRIPTION', 'Manage access rights for content'),
2978
                'help' => _t('SiteTree.PERMISSION_GRANTACCESS_HELP', 'Allow setting of page-specific access restrictions in the "Pages" section.'),
2979
                'category' => _t('Permissions.PERMISSIONS_CATEGORY', 'Roles and access permissions'),
2980
                'sort' => 100
2981
            ),
2982
            'SITETREE_VIEW_ALL' => array(
2983
                'name' => _t('SiteTree.VIEW_ALL_DESCRIPTION', 'View any page'),
2984
                'category' => _t('Permissions.CONTENT_CATEGORY', 'Content permissions'),
2985
                'sort' => -100,
2986
                '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')
2987
            ),
2988
            'SITETREE_EDIT_ALL' => array(
2989
                'name' => _t('SiteTree.EDIT_ALL_DESCRIPTION', 'Edit any page'),
2990
                'category' => _t('Permissions.CONTENT_CATEGORY', 'Content permissions'),
2991
                'sort' => -50,
2992
                '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')
2993
            ),
2994
            'SITETREE_REORGANISE' => array(
2995
                'name' => _t('SiteTree.REORGANISE_DESCRIPTION', 'Change site structure'),
2996
                'category' => _t('Permissions.CONTENT_CATEGORY', 'Content permissions'),
2997
                'help' => _t('SiteTree.REORGANISE_HELP', 'Rearrange pages in the site tree through drag&drop.'),
2998
                'sort' => 100
2999
            ),
3000
            'VIEW_DRAFT_CONTENT' => array(
3001
                'name' => _t('SiteTree.VIEW_DRAFT_CONTENT', 'View draft content'),
3002
                'category' => _t('Permissions.CONTENT_CATEGORY', 'Content permissions'),
3003
                '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.'),
3004
                'sort' => 100
3005
            )
3006
        );
3007
    }
3008
3009
    /**
3010
     * Return the translated Singular name.
3011
     *
3012
     * @return string
3013
     */
3014
    public function i18n_singular_name()
3015
    {
3016
        // Convert 'Page' to 'SiteTree' for correct localization lookups
3017
        /** @skipUpgrade */
3018
        // @todo When we namespace translations, change 'SiteTree' to FQN of the class
3019
        $class = (static::class == 'Page' || static::class === self::class)
3020
            ? 'SiteTree'
3021
            : static::class;
3022
        return _t($class.'.SINGULARNAME', $this->singular_name());
3023
    }
3024
3025
    /**
3026
     * Overloaded to also provide entities for 'Page' class which is usually located in custom code, hence textcollector
3027
     * picks it up for the wrong folder.
3028
     *
3029
     * @return array
3030
     */
3031
    public function provideI18nEntities()
3032
    {
3033
        $entities = parent::provideI18nEntities();
3034
3035
        if (isset($entities['Page.SINGULARNAME'])) {
3036
            $entities['Page.SINGULARNAME'][3] = CMS_DIR;
3037
        }
3038
        if (isset($entities['Page.PLURALNAME'])) {
3039
            $entities['Page.PLURALNAME'][3] = CMS_DIR;
3040
        }
3041
3042
        $entities[static::class . '.DESCRIPTION'] = array(
3043
            $this->stat('description'),
3044
            'Description of the page type (shown in the "add page" dialog)'
3045
        );
3046
3047
        $entities['SiteTree.SINGULARNAME'][0] = 'Page';
3048
        $entities['SiteTree.PLURALNAME'][0] = 'Pages';
3049
3050
        return $entities;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $entities; (array<*,null|array>) is incompatible with the return type of the parent method SilverStripe\ORM\DataObject::provideI18nEntities of type array<*,string[]>.

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...
3051
    }
3052
3053
    /**
3054
     * Returns 'root' if the current page has no parent, or 'subpage' otherwise
3055
     *
3056
     * @return string
3057
     */
3058
    public function getParentType()
3059
    {
3060
        return $this->ParentID == 0 ? 'root' : 'subpage';
0 ignored issues
show
Documentation introduced by
The property ParentID does not exist on object<SilverStripe\CMS\Model\SiteTree>. Since you implemented __get, maybe consider adding a @property annotation.

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

<?php

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

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

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

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

}

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

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

See also the PhpDoc documentation for @property.

Loading history...
3061
    }
3062
3063
    /**
3064
     * Clear the permissions cache for SiteTree
3065
     */
3066
    public static function reset()
3067
    {
3068
        self::$cache_permissions = array();
3069
    }
3070
3071
    public static function on_db_reset()
3072
    {
3073
        self::$cache_permissions = array();
3074
    }
3075
}
3076