Completed
Pull Request — master (#1882)
by
unknown
01:54
created

SiteTree::collateDescendants()   B

Complexity

Conditions 5
Paths 5

Size

Total Lines 21
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 21
rs 8.7624
c 0
b 0
f 0
cc 5
eloc 12
nc 5
nop 2
1
<?php
2
3
namespace SilverStripe\CMS\Model;
4
5
use Page;
6
use SilverStripe\CampaignAdmin\AddToCampaignHandler_FormAction;
7
use SilverStripe\CMS\Controllers\CMSPageEditController;
8
use SilverStripe\CMS\Controllers\ContentController;
9
use SilverStripe\CMS\Controllers\ModelAsController;
10
use SilverStripe\CMS\Controllers\RootURLController;
11
use SilverStripe\CMS\Forms\SiteTreeURLSegmentField;
12
use SilverStripe\Control\ContentNegotiator;
13
use SilverStripe\Control\Controller;
14
use SilverStripe\Control\Director;
15
use SilverStripe\Control\RequestHandler;
16
use SilverStripe\Core\ClassInfo;
17
use SilverStripe\Core\Config\Config;
18
use SilverStripe\Core\Convert;
19
use SilverStripe\Core\Injector\Injector;
20
use SilverStripe\Core\Resettable;
21
use SilverStripe\Dev\Deprecation;
22
use SilverStripe\Forms\CheckboxField;
23
use SilverStripe\Forms\CompositeField;
24
use SilverStripe\Forms\DropdownField;
25
use SilverStripe\Forms\FieldGroup;
26
use SilverStripe\Forms\FieldList;
27
use SilverStripe\Forms\FormAction;
28
use SilverStripe\Forms\GridField\GridField;
29
use SilverStripe\Forms\GridField\GridFieldDataColumns;
30
use SilverStripe\Forms\HTMLEditor\HTMLEditorField;
31
use SilverStripe\Forms\ListboxField;
32
use SilverStripe\Forms\LiteralField;
33
use SilverStripe\Forms\OptionsetField;
34
use SilverStripe\Forms\Tab;
35
use SilverStripe\Forms\TabSet;
36
use SilverStripe\Forms\TextareaField;
37
use SilverStripe\Forms\TextField;
38
use SilverStripe\Forms\ToggleCompositeField;
39
use SilverStripe\Forms\TreeDropdownField;
40
use SilverStripe\i18n\i18n;
41
use SilverStripe\i18n\i18nEntityProvider;
42
use SilverStripe\ORM\ArrayList;
43
use SilverStripe\ORM\CMSPreviewable;
44
use SilverStripe\ORM\DataList;
45
use SilverStripe\ORM\DataObject;
46
use SilverStripe\ORM\DB;
47
use SilverStripe\ORM\HiddenClass;
48
use SilverStripe\ORM\Hierarchy\Hierarchy;
49
use SilverStripe\ORM\ManyManyList;
50
use SilverStripe\ORM\ValidationResult;
51
use SilverStripe\Security\Group;
52
use SilverStripe\Security\InheritedPermissions;
53
use SilverStripe\Security\InheritedPermissionsExtension;
54
use SilverStripe\Security\Member;
55
use SilverStripe\Security\Permission;
56
use SilverStripe\Security\PermissionChecker;
57
use SilverStripe\Security\PermissionProvider;
58
use SilverStripe\Security\Security;
59
use SilverStripe\SiteConfig\SiteConfig;
60
use SilverStripe\Versioned\Versioned;
61
use SilverStripe\View\ArrayData;
62
use SilverStripe\View\HTML;
63
use SilverStripe\View\Parsers\ShortcodeParser;
64
use SilverStripe\View\Parsers\URLSegmentFilter;
65
use SilverStripe\View\SSViewer;
66
use Subsite;
67
68
/**
69
 * Basic data-object representing all pages within the site tree. All page types that live within the hierarchy should
70
 * inherit from this. In addition, it contains a number of static methods for querying the site tree and working with
71
 * draft and published states.
72
 *
73
 * <h2>URLs</h2>
74
 * A page is identified during request handling via its "URLSegment" database column. As pages can be nested, the full
75
 * path of a URL might contain multiple segments. Each segment is stored in its filtered representation (through
76
 * {@link URLSegmentFilter}). The full path is constructed via {@link Link()}, {@link RelativeLink()} and
77
 * {@link AbsoluteLink()}. You can allow these segments to contain multibyte characters through
78
 * {@link URLSegmentFilter::$default_allow_multibyte}.
79
 *
80
 * @property string URLSegment
81
 * @property string Title
82
 * @property string MenuTitle
83
 * @property string Content HTML content of the page.
84
 * @property string MetaDescription
85
 * @property string ExtraMeta
86
 * @property string ShowInMenus
87
 * @property string ShowInSearch
88
 * @property string Sort Integer value denoting the sort order.
89
 * @property string ReportClass
90
 *
91
 * @method ManyManyList ViewerGroups() List of groups that can view this object.
92
 * @method ManyManyList EditorGroups() List of groups that can edit this object.
93
 * @method SiteTree Parent()
94
 *
95
 * @mixin Hierarchy
96
 * @mixin Versioned
97
 * @mixin SiteTreeLinkTracking
98
 * @mixin InheritedPermissionsExtension
99
 */
100
class SiteTree extends DataObject implements PermissionProvider, i18nEntityProvider, CMSPreviewable, Resettable
101
{
102
103
    /**
104
     * Indicates what kind of children this page type can have.
105
     * This can be an array of allowed child classes, or the string "none" -
106
     * indicating that this page type can't have children.
107
     * If a classname is prefixed by "*", such as "*Page", then only that
108
     * class is allowed - no subclasses. Otherwise, the class and all its
109
     * subclasses are allowed.
110
     * To control allowed children on root level (no parent), use {@link $can_be_root}.
111
     *
112
     * Note that this setting is cached when used in the CMS, use the "flush" query parameter to clear it.
113
     *
114
     * @config
115
     * @var array
116
     */
117
    private static $allowed_children = [
118
        self::class
119
    ];
120
121
    /**
122
     * The default child class for this page.
123
     * Note: Value might be cached, see {@link $allowed_chilren}.
124
     *
125
     * @config
126
     * @var string
127
     */
128
    private static $default_child = "Page";
129
130
    /**
131
     * Default value for SiteTree.ClassName enum
132
     * {@see DBClassName::getDefault}
133
     *
134
     * @config
135
     * @var string
136
     */
137
    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...
138
139
    /**
140
     * The default parent class for this page.
141
     * Note: Value might be cached, see {@link $allowed_chilren}.
142
     *
143
     * @config
144
     * @var string
145
     */
146
    private static $default_parent = null;
147
148
    /**
149
     * Controls whether a page can be in the root of the site tree.
150
     * Note: Value might be cached, see {@link $allowed_chilren}.
151
     *
152
     * @config
153
     * @var bool
154
     */
155
    private static $can_be_root = true;
156
157
    /**
158
     * List of permission codes a user can have to allow a user to create a page of this type.
159
     * Note: Value might be cached, see {@link $allowed_chilren}.
160
     *
161
     * @config
162
     * @var array
163
     */
164
    private static $need_permission = null;
165
166
    /**
167
     * If you extend a class, and don't want to be able to select the old class
168
     * in the cms, set this to the old class name. Eg, if you extended Product
169
     * to make ImprovedProduct, then you would set $hide_ancestor to Product.
170
     *
171
     * @config
172
     * @var string
173
     */
174
    private static $hide_ancestor = null;
175
176
    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...
177
        "URLSegment" => "Varchar(255)",
178
        "Title" => "Varchar(255)",
179
        "MenuTitle" => "Varchar(100)",
180
        "Content" => "HTMLText",
181
        "MetaDescription" => "Text",
182
        "ExtraMeta" => "HTMLFragment(['whitelist' => ['meta', 'link']])",
183
        "ShowInMenus" => "Boolean",
184
        "ShowInSearch" => "Boolean",
185
        "Sort" => "Int",
186
        "HasBrokenFile" => "Boolean",
187
        "HasBrokenLink" => "Boolean",
188
        "ReportClass" => "Varchar",
189
    );
190
191
    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...
192
        "URLSegment" => true,
193
    );
194
195
    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...
196
        "VirtualPages" => "SilverStripe\\CMS\\Model\\VirtualPage.CopyContentFrom"
197
    );
198
199
    private static $owned_by = array(
200
        "VirtualPages"
201
    );
202
203
    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...
204
        "Breadcrumbs" => "HTMLFragment",
205
        "LastEdited" => "Datetime",
206
        "Created" => "Datetime",
207
        'Link' => 'Text',
208
        'RelativeLink' => 'Text',
209
        'AbsoluteLink' => 'Text',
210
        'CMSEditLink' => 'Text',
211
        'TreeTitle' => 'HTMLFragment',
212
        'MetaTags' => 'HTMLFragment',
213
    );
214
215
    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...
216
        "ShowInMenus" => 1,
217
        "ShowInSearch" => 1,
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
    private static $extensions = [
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...
246
        Hierarchy::class,
247
        Versioned::class,
248
        SiteTreeLinkTracking::class,
249
        InheritedPermissionsExtension::class,
250
    ];
251
252
    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...
253
        'Title',
254
        'Content',
255
    );
256
257
    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...
258
        'URLSegment' => 'URL'
259
    );
260
261
    /**
262
     * @config
263
     */
264
    private static $nested_urls = true;
265
266
    /**
267
     * @config
268
    */
269
    private static $create_default_pages = true;
270
271
    /**
272
     * This controls whether of not extendCMSFields() is called by getCMSFields.
273
     */
274
    private static $runCMSFieldsExtensions = true;
275
276
    /**
277
     * @config
278
     * @var boolean
279
     */
280
    private static $enforce_strict_hierarchy = true;
281
282
    /**
283
     * The value used for the meta generator tag. Leave blank to omit the tag.
284
     *
285
     * @config
286
     * @var string
287
     */
288
    private static $meta_generator = 'SilverStripe - http://silverstripe.org';
289
290
    protected $_cache_statusFlags = null;
291
292
    /**
293
     * Plural form for SiteTree / Page classes. Not inherited by subclasses.
294
     *
295
     * @config
296
     * @var string
297
     */
298
    private static $base_plural_name = 'Pages';
299
300
    /**
301
     * Plural form for SiteTree / Page classes. Not inherited by subclasses.
302
     *
303
     * @config
304
     * @var string
305
     */
306
    private static $base_singular_name = 'Page';
307
308
    /**
309
     * Description of the class functionality, typically shown to a user
310
     * when selecting which page type to create. Translated through {@link provideI18nEntities()}.
311
     *
312
     * @see SiteTree::classDescription()
313
     * @see SiteTree::i18n_classDescription()
314
     *
315
     * @config
316
     * @var string
317
     */
318
    private static $description = null;
319
320
    /**
321
     * Description for Page and SiteTree classes, but not inherited by subclasses.
322
     * override SiteTree::$description in subclasses instead.
323
     *
324
     * @see SiteTree::classDescription()
325
     * @see SiteTree::i18n_classDescription()
326
     *
327
     * @config
328
     * @var string
329
     */
330
    private static $base_description = 'Generic content page';
331
332
    /**
333
     * Fetches the {@link SiteTree} object that maps to a link.
334
     *
335
     * If you have enabled {@link SiteTree::config()->nested_urls} on this site, then you can use a nested link such as
336
     * "about-us/staff/", and this function will traverse down the URL chain and grab the appropriate link.
337
     *
338
     * Note that if no model can be found, this method will fall over to a extended alternateGetByLink method provided
339
     * by a extension attached to {@link SiteTree}
340
     *
341
     * @param string $link  The link of the page to search for
342
     * @param bool   $cache True (default) to use caching, false to force a fresh search from the database
343
     * @return SiteTree
344
     */
345
    public static function get_by_link($link, $cache = true)
346
    {
347
        if (trim($link, '/')) {
348
            $link = trim(Director::makeRelative($link), '/');
349
        } else {
350
            $link = RootURLController::get_homepage_link();
351
        }
352
353
        $parts = preg_split('|/+|', $link);
354
355
        // Grab the initial root level page to traverse down from.
356
        $URLSegment = array_shift($parts);
357
        $conditions = array('"SiteTree"."URLSegment"' => rawurlencode($URLSegment));
358
        if (self::config()->nested_urls) {
359
            $conditions[] = array('"SiteTree"."ParentID"' => 0);
360
        }
361
        /** @var SiteTree $sitetree */
362
        $sitetree = DataObject::get_one(self::class, $conditions, $cache);
363
364
        /// Fall back on a unique URLSegment for b/c.
365
        if (!$sitetree
366
            && self::config()->nested_urls
367
            && $sitetree = DataObject::get_one(self::class, array(
368
                '"SiteTree"."URLSegment"' => $URLSegment
369
            ), $cache)
370
        ) {
371
            return $sitetree;
372
        }
373
374
        // Attempt to grab an alternative page from extensions.
375
        if (!$sitetree) {
376
            $parentID = self::config()->nested_urls ? 0 : null;
377
378 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...
379
                foreach ($alternatives as $alternative) {
380
                    if ($alternative) {
381
                        $sitetree = $alternative;
382
                    }
383
                }
384
            }
385
386
            if (!$sitetree) {
387
                return null;
388
            }
389
        }
390
391
        // Check if we have any more URL parts to parse.
392
        if (!self::config()->nested_urls || !count($parts)) {
393
            return $sitetree;
394
        }
395
396
        // Traverse down the remaining URL segments and grab the relevant SiteTree objects.
397
        foreach ($parts as $segment) {
398
            $next = DataObject::get_one(
399
                self::class,
400
                array(
401
                    '"SiteTree"."URLSegment"' => $segment,
402
                    '"SiteTree"."ParentID"' => $sitetree->ID
403
                ),
404
                $cache
405
            );
406
407
            if (!$next) {
408
                $parentID = (int) $sitetree->ID;
409
410 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...
411
                    foreach ($alternatives as $alternative) {
412
                        if ($alternative) {
413
                            $next = $alternative;
414
                        }
415
                    }
416
                }
417
418
                if (!$next) {
419
                    return null;
420
                }
421
            }
422
423
            $sitetree->destroy();
424
            $sitetree = $next;
425
        }
426
427
        return $sitetree;
428
    }
429
430
    /**
431
     * Return a subclass map of SiteTree that shouldn't be hidden through {@link SiteTree::$hide_ancestor}
432
     *
433
     * @return array
434
     */
435
    public static function page_type_classes()
436
    {
437
        $classes = ClassInfo::getValidSubClasses();
438
439
        $baseClassIndex = array_search(self::class, $classes);
440
        if ($baseClassIndex !== false) {
441
            unset($classes[$baseClassIndex]);
442
        }
443
444
        $kill_ancestors = array();
445
446
        // figure out if there are any classes we don't want to appear
447
        foreach ($classes as $class) {
448
            $instance = singleton($class);
449
450
            // do any of the progeny want to hide an ancestor?
451
            if ($ancestor_to_hide = $instance->stat('hide_ancestor')) {
452
                // note for killing later
453
                $kill_ancestors[] = $ancestor_to_hide;
454
            }
455
        }
456
457
        // If any of the descendents don't want any of the elders to show up, cruelly render the elders surplus to
458
        // requirements
459
        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...
460
            $kill_ancestors = array_unique($kill_ancestors);
461
            foreach ($kill_ancestors as $mark) {
462
                // unset from $classes
463
                $idx = array_search($mark, $classes, true);
464
                if ($idx !== false) {
465
                    unset($classes[$idx]);
466
                }
467
            }
468
        }
469
470
        return $classes;
471
    }
472
473
    /**
474
     * Replace a "[sitetree_link id=n]" shortcode with a link to the page with the corresponding ID.
475
     *
476
     * @param array      $arguments
477
     * @param string     $content
478
     * @param ShortcodeParser $parser
479
     * @return string
480
     */
481
    public static function link_shortcode_handler($arguments, $content = null, $parser = null)
482
    {
483
        if (!isset($arguments['id']) || !is_numeric($arguments['id'])) {
484
            return null;
485
        }
486
487
        /** @var SiteTree $page */
488
        if (!($page = DataObject::get_by_id(self::class, $arguments['id']))         // Get the current page by ID.
489
            && !($page = Versioned::get_latest_version(self::class, $arguments['id'])) // Attempt link to old version.
490
        ) {
491
             return null; // There were no suitable matches at all.
492
        }
493
494
        /** @var SiteTree $page */
495
        $link = Convert::raw2att($page->Link());
496
497
        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...
498
            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...
499
        } else {
500
            return $link;
501
        }
502
    }
503
504
    /**
505
     * Return the link for this {@link SiteTree} object, with the {@link Director::baseURL()} included.
506
     *
507
     * @param string $action Optional controller action (method).
508
     *                       Note: URI encoding of this parameter is applied automatically through template casting,
509
     *                       don't encode the passed parameter. Please use {@link Controller::join_links()} instead to
510
     *                       append GET parameters.
511
     * @return string
512
     */
513
    public function Link($action = null)
514
    {
515
        return Controller::join_links(Director::baseURL(), $this->RelativeLink($action));
516
    }
517
518
    /**
519
     * Get the absolute URL for this page, including protocol and host.
520
     *
521
     * @param string $action See {@link Link()}
522
     * @return string
523
     */
524
    public function AbsoluteLink($action = null)
525
    {
526
        if ($this->hasMethod('alternateAbsoluteLink')) {
527
            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...
528
        } else {
529
            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 529 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...
530
        }
531
    }
532
533
    /**
534
     * Base link used for previewing. Defaults to absolute URL, in order to account for domain changes, e.g. on multi
535
     * site setups. Does not contain hints about the stage, see {@link SilverStripeNavigator} for details.
536
     *
537
     * @param string $action See {@link Link()}
538
     * @return string
539
     */
540
    public function PreviewLink($action = null)
541
    {
542
        if ($this->hasMethod('alternatePreviewLink')) {
543
            Deprecation::notice('5.0', 'Use updatePreviewLink or override PreviewLink method');
544
            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...
545
        }
546
547
        $link = $this->AbsoluteLink($action);
548
        $this->extend('updatePreviewLink', $link, $action);
549
        return $link;
550
    }
551
552
    public function getMimeType()
553
    {
554
        return 'text/html';
555
    }
556
557
    /**
558
     * Return the link for this {@link SiteTree} object relative to the SilverStripe root.
559
     *
560
     * By default, if this page is the current home page, and there is no action specified then this will return a link
561
     * to the root of the site. However, if you set the $action parameter to TRUE then the link will not be rewritten
562
     * and returned in its full form.
563
     *
564
     * @uses RootURLController::get_homepage_link()
565
     *
566
     * @param string $action See {@link Link()}
567
     * @return string
568
     */
569
    public function RelativeLink($action = null)
570
    {
571
        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...
572
            $parent = $this->Parent();
573
            // If page is removed select parent from version history (for archive page view)
574
            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...
575
                $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...
576
            }
577
            $base = $parent->RelativeLink($this->URLSegment);
578
        } 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...
579
            // Unset base for root-level homepages.
580
            // Note: Homepages with action parameters (or $action === true)
581
            // need to retain their URLSegment.
582
            $base = null;
583
        } else {
584
            $base = $this->URLSegment;
585
        }
586
587
        $this->extend('updateRelativeLink', $base, $action);
588
589
        // Legacy support: If $action === true, retain URLSegment for homepages,
590
        // but don't append any action
591
        if ($action === true) {
592
            $action = null;
593
        }
594
595
        return Controller::join_links($base, '/', $action);
596
    }
597
598
    /**
599
     * Get the absolute URL for this page on the Live site.
600
     *
601
     * @param bool $includeStageEqualsLive Whether to append the URL with ?stage=Live to force Live mode
602
     * @return string
603
     */
604
    public function getAbsoluteLiveLink($includeStageEqualsLive = true)
605
    {
606
        $oldReadingMode = Versioned::get_reading_mode();
607
        Versioned::set_stage(Versioned::LIVE);
608
        /** @var SiteTree $live */
609
        $live = Versioned::get_one_by_stage(self::class, Versioned::LIVE, array(
610
            '"SiteTree"."ID"' => $this->ID
611
        ));
612
        if ($live) {
613
            $link = $live->AbsoluteLink();
614
            if ($includeStageEqualsLive) {
615
                $link = Controller::join_links($link, '?stage=Live');
616
            }
617
        } else {
618
            $link = null;
619
        }
620
621
        Versioned::set_reading_mode($oldReadingMode);
622
        return $link;
623
    }
624
625
    /**
626
     * Generates a link to edit this page in the CMS.
627
     *
628
     * @return string
629
     */
630
    public function CMSEditLink()
631
    {
632
        $link = Controller::join_links(
633
            CMSPageEditController::singleton()->Link('show'),
634
            $this->ID
635
        );
636
        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 636 which is incompatible with the return type declared by the interface SilverStripe\ORM\CMSPreviewable::CMSEditLink of type string. It seems like you forgot to handle an error condition.
Loading history...
637
    }
638
639
640
    /**
641
     * Return a CSS identifier generated from this page's link.
642
     *
643
     * @return string The URL segment
644
     */
645
    public function ElementName()
646
    {
647
        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...
648
    }
649
650
    /**
651
     * Returns true if this is the currently active page being used to handle this request.
652
     *
653
     * @return bool
654
     */
655
    public function isCurrent()
656
    {
657
        $currentPage = Director::get_current_page();
658
        if ($currentPage instanceof ContentController) {
659
            $currentPage = $currentPage->data();
660
        }
661
        if ($currentPage instanceof SiteTree) {
662
            return $currentPage === $this || $currentPage->ID === $this->ID;
663
        }
664
        return false;
665
    }
666
667
    /**
668
     * Check if this page is in the currently active section (e.g. it is either current or one of its children is
669
     * currently being viewed).
670
     *
671
     * @return bool
672
     */
673
    public function isSection()
674
    {
675
        return $this->isCurrent() || (
676
            Director::get_current_page() instanceof SiteTree && in_array($this->ID, Director::get_current_page()->getAncestors()->column())
677
        );
678
    }
679
680
    /**
681
     * Check if the parent of this page has been removed (or made otherwise unavailable), and is still referenced by
682
     * this child. Any such orphaned page may still require access via the CMS, but should not be shown as accessible
683
     * to external users.
684
     *
685
     * @return bool
686
     */
687
    public function isOrphaned()
688
    {
689
        // Always false for root pages
690
        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...
691
            return false;
692
        }
693
694
        // Parent must exist and not be an orphan itself
695
        $parent = $this->Parent();
696
        return !$parent || !$parent->exists() || $parent->isOrphaned();
697
    }
698
699
    /**
700
     * Return "link" or "current" depending on if this is the {@link SiteTree::isCurrent()} current page.
701
     *
702
     * @return string
703
     */
704
    public function LinkOrCurrent()
705
    {
706
        return $this->isCurrent() ? 'current' : 'link';
707
    }
708
709
    /**
710
     * Return "link" or "section" depending on if this is the {@link SiteTree::isSeciton()} current section.
711
     *
712
     * @return string
713
     */
714
    public function LinkOrSection()
715
    {
716
        return $this->isSection() ? 'section' : 'link';
717
    }
718
719
    /**
720
     * Return "link", "current" or "section" depending on if this page is the current page, or not on the current page
721
     * but in the current section.
722
     *
723
     * @return string
724
     */
725
    public function LinkingMode()
726
    {
727
        if ($this->isCurrent()) {
728
            return 'current';
729
        } elseif ($this->isSection()) {
730
            return 'section';
731
        } else {
732
            return 'link';
733
        }
734
    }
735
736
    /**
737
     * Check if this page is in the given current section.
738
     *
739
     * @param string $sectionName Name of the section to check
740
     * @return bool True if we are in the given section
741
     */
742
    public function InSection($sectionName)
743
    {
744
        $page = Director::get_current_page();
745
        while ($page && $page->exists()) {
746
            if ($sectionName == $page->URLSegment) {
747
                return true;
748
            }
749
            $page = $page->Parent();
750
        }
751
        return false;
752
    }
753
754
    /**
755
     * Reset Sort on duped page
756
     *
757
     * @param SiteTree $original
758
     * @param bool $doWrite
759
     */
760
    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...
761
    {
762
        $this->Sort = 0;
763
    }
764
765
    /**
766
     * Duplicates each child of this node recursively and returns the top-level duplicate node.
767
     *
768
     * @return static The duplicated object
769
     */
770
    public function duplicateWithChildren()
771
    {
772
        /** @var SiteTree $clone */
773
        $clone = $this->duplicate();
774
        $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...
775
776
        if ($children) {
777
            /** @var SiteTree $child */
778
            $sort = 0;
779
            foreach ($children as $child) {
780
                $childClone = method_exists($child, 'duplicateWithChildren')
781
                    ? $child->duplicateWithChildren()
782
                    : $child->duplicate();
783
                $childClone->ParentID = $clone->ID;
784
                //retain sort order by manually setting sort values
785
                $childClone->Sort = ++$sort;
786
                $childClone->write();
787
            }
788
        }
789
790
        return $clone;
791
    }
792
793
    /**
794
     * Duplicate this node and its children as a child of the node with the given ID
795
     *
796
     * @param int $id ID of the new node's new parent
797
     */
798
    public function duplicateAsChild($id)
799
    {
800
        /** @var SiteTree $newSiteTree */
801
        $newSiteTree = $this->duplicate();
802
        $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...
803
        $newSiteTree->Sort = 0;
804
        $newSiteTree->write();
805
    }
806
807
    /**
808
     * Return a breadcrumb trail to this page. Excludes "hidden" pages (with ShowInMenus=0) by default.
809
     *
810
     * @param int $maxDepth The maximum depth to traverse.
811
     * @param boolean $unlinked Whether to link page titles.
812
     * @param boolean|string $stopAtPageType ClassName of a page to stop the upwards traversal.
813
     * @param boolean $showHidden Include pages marked with the attribute ShowInMenus = 0
814
     * @return string The breadcrumb trail.
815
     */
816
    public function Breadcrumbs($maxDepth = 20, $unlinked = false, $stopAtPageType = false, $showHidden = false)
817
    {
818
        $pages = $this->getBreadcrumbItems($maxDepth, $stopAtPageType, $showHidden);
819
        $template = new SSViewer('BreadcrumbsTemplate');
820
        return $template->process($this->customise(new ArrayData(array(
821
            "Pages" => $pages,
822
            "Unlinked" => $unlinked
823
        ))));
824
    }
825
826
827
    /**
828
     * Returns a list of breadcrumbs for the current page.
829
     *
830
     * @param int $maxDepth The maximum depth to traverse.
831
     * @param boolean|string $stopAtPageType ClassName of a page to stop the upwards traversal.
832
     * @param boolean $showHidden Include pages marked with the attribute ShowInMenus = 0
833
     *
834
     * @return ArrayList
835
    */
836
    public function getBreadcrumbItems($maxDepth = 20, $stopAtPageType = false, $showHidden = false)
837
    {
838
        $page = $this;
839
        $pages = array();
840
841
        while ($page
842
            && $page->exists()
843
            && (!$maxDepth || count($pages) < $maxDepth)
844
            && (!$stopAtPageType || $page->ClassName != $stopAtPageType)
845
        ) {
846
            if ($showHidden || $page->ShowInMenus || ($page->ID == $this->ID)) {
847
                $pages[] = $page;
848
            }
849
850
            $page = $page->Parent();
851
        }
852
853
        return new ArrayList(array_reverse($pages));
854
    }
855
856
857
    /**
858
     * Make this page a child of another page.
859
     *
860
     * If the parent page does not exist, resolve it to a valid ID before updating this page's reference.
861
     *
862
     * @param SiteTree|int $item Either the parent object, or the parent ID
863
     */
864
    public function setParent($item)
865
    {
866
        if (is_object($item)) {
867
            if (!$item->exists()) {
868
                $item->write();
869
            }
870
            $this->setField("ParentID", $item->ID);
871
        } else {
872
            $this->setField("ParentID", $item);
873
        }
874
    }
875
876
    /**
877
     * Get the parent of this page.
878
     *
879
     * @return SiteTree Parent of this page
880
     */
881
    public function getParent()
882
    {
883
        if ($parentID = $this->getField("ParentID")) {
884
            return DataObject::get_by_id(self::class, $parentID);
885
        }
886
        return null;
887
    }
888
889
    /**
890
     * Return a string of the form "parent - page" or "grandparent - parent - page" using page titles
891
     *
892
     * @param int $level The maximum amount of levels to traverse.
893
     * @param string $separator Seperating string
894
     * @return string The resulting string
895
     */
896
    public function NestedTitle($level = 2, $separator = " - ")
897
    {
898
        $item = $this;
899
        $parts = [];
900
        while ($item && $level > 0) {
901
            $parts[] = $item->Title;
902
            $item = $item->getParent();
903
            $level--;
904
        }
905
        return implode($separator, array_reverse($parts));
906
    }
907
908
    /**
909
     * This function should return true if the current user can execute this action. It can be overloaded to customise
910
     * the security model for an application.
911
     *
912
     * Slightly altered from parent behaviour in {@link DataObject->can()}:
913
     * - Checks for existence of a method named "can<$perm>()" on the object
914
     * - Calls decorators and only returns for FALSE "vetoes"
915
     * - Falls back to {@link Permission::check()}
916
     * - Does NOT check for many-many relations named "Can<$perm>"
917
     *
918
     * @uses DataObjectDecorator->can()
919
     *
920
     * @param string $perm The permission to be checked, such as 'View'
921
     * @param Member $member The member whose permissions need checking. Defaults to the currently logged in user.
922
     * @param array $context Context argument for canCreate()
923
     * @return bool True if the the member is allowed to do the given action
924
     */
925
    public function can($perm, $member = null, $context = array())
926
    {
927
        if (!$member) {
928
            $member = Security::getCurrentUser();
929
        }
930
931
        if ($member && Permission::checkMember($member, "ADMIN")) {
932
            return true;
933
        }
934
935
        if (is_string($perm) && method_exists($this, 'can' . ucfirst($perm))) {
936
            $method = 'can' . ucfirst($perm);
937
            return $this->$method($member);
938
        }
939
940
        $results = $this->extend('can', $member);
941
        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...
942
            if (!min($results)) {
943
                return false;
944
            }
945
        }
946
947
        return ($member && Permission::checkMember($member, $perm));
948
    }
949
950
    /**
951
     * This function should return true if the current user can add children to this page. It can be overloaded to
952
     * customise the security model for an application.
953
     *
954
     * Denies permission if any of the following conditions is true:
955
     * - alternateCanAddChildren() on a extension returns false
956
     * - canEdit() is not granted
957
     * - There are no classes defined in {@link $allowed_children}
958
     *
959
     * @uses SiteTreeExtension->canAddChildren()
960
     * @uses canEdit()
961
     * @uses $allowed_children
962
     *
963
     * @param Member|int $member
964
     * @return bool True if the current user can add children
965
     */
966
    public function canAddChildren($member = null)
967
    {
968
        // Disable adding children to archived pages
969
        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...
970
            return false;
971
        }
972
973
        if (!$member) {
974
            $member = Security::getCurrentUser();
975
        }
976
977
        // Standard mechanism for accepting permission changes from extensions
978
        $extended = $this->extendedCan('canAddChildren', $member);
0 ignored issues
show
Bug introduced by
It seems like $member can also be of type null; however, SilverStripe\ORM\DataObject::extendedCan() does only seem to accept object<SilverStripe\Security\Member>|integer, 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...
979
        if ($extended !== null) {
980
            return $extended;
981
        }
982
983
        // Default permissions
984
        if ($member && Permission::checkMember($member, "ADMIN")) {
985
            return true;
986
        }
987
988
        return $this->canEdit($member) && $this->stat('allowed_children') !== 'none';
0 ignored issues
show
Bug introduced by
It seems like $member defined by parameter $member on line 966 can also be of type integer; however, SilverStripe\CMS\Model\SiteTree::canEdit() does only seem to accept object<SilverStripe\Security\Member>|null, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
989
    }
990
991
    /**
992
     * This function should return true if the current user can view this page. It can be overloaded to customise the
993
     * security model for an application.
994
     *
995
     * Denies permission if any of the following conditions is true:
996
     * - canView() on any extension returns false
997
     * - "CanViewType" directive is set to "Inherit" and any parent page return false for canView()
998
     * - "CanViewType" directive is set to "LoggedInUsers" and no user is logged in
999
     * - "CanViewType" directive is set to "OnlyTheseUsers" and user is not in the given groups
1000
     *
1001
     * @uses DataExtension->canView()
1002
     * @uses ViewerGroups()
1003
     *
1004
     * @param Member $member
1005
     * @return bool True if the current user can view this page
1006
     */
1007
    public function canView($member = null)
1008
    {
1009
        if (!$member) {
1010
            $member = Security::getCurrentUser();
1011
        }
1012
1013
        // Standard mechanism for accepting permission changes from extensions
1014
        $extended = $this->extendedCan('canView', $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...
1015
        if ($extended !== null) {
1016
            return $extended;
1017
        }
1018
1019
        // admin override
1020
        if ($member && Permission::checkMember($member, array("ADMIN", "SITETREE_VIEW_ALL"))) {
1021
            return true;
1022
        }
1023
1024
        // Orphaned pages (in the current stage) are unavailable, except for admins via the CMS
1025
        if ($this->isOrphaned()) {
1026
            return false;
1027
        }
1028
1029
        // Note: getInheritedPermissions() is disused in this instance
1030
        // to allow parent canView extensions to influence subpage canView()
1031
1032
        // check for empty spec
1033
        if (!$this->CanViewType || $this->CanViewType === InheritedPermissions::ANYONE) {
0 ignored issues
show
Documentation introduced by
The property CanViewType 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...
1034
            return true;
1035
        }
1036
1037
        // check for inherit
1038
        if ($this->CanViewType === InheritedPermissions::INHERIT) {
0 ignored issues
show
Documentation introduced by
The property CanViewType 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...
1039
            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...
1040
                return $this->Parent()->canView($member);
1041
            } else {
1042
                return $this->getSiteConfig()->canViewPages($member);
1043
            }
1044
        }
1045
1046
        // check for any logged-in users
1047
        if ($this->CanViewType === InheritedPermissions::LOGGED_IN_USERS && $member && $member->ID) {
0 ignored issues
show
Documentation introduced by
The property CanViewType 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...
1048
            return true;
1049
        }
1050
1051
        // check for specific groups
1052
        if ($this->CanViewType === InheritedPermissions::ONLY_THESE_USERS
0 ignored issues
show
Documentation introduced by
The property CanViewType 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...
1053
            && $member
1054
            && $member->inGroups($this->ViewerGroups())
1055
        ) {
1056
            return true;
1057
        }
1058
1059
        return false;
1060
    }
1061
1062
    /**
1063
     * Check if this page can be published
1064
     *
1065
     * @param Member $member
1066
     * @return bool
1067
     */
1068 View Code Duplication
    public function canPublish($member = null)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in 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...
1069
    {
1070
        if (!$member) {
1071
            $member = Security::getCurrentUser();
1072
        }
1073
1074
        // Check extension
1075
        $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...
1076
        if ($extended !== null) {
1077
            return $extended;
1078
        }
1079
1080
        if (Permission::checkMember($member, "ADMIN")) {
1081
            return true;
1082
        }
1083
1084
        // Default to relying on edit permission
1085
        return $this->canEdit($member);
1086
    }
1087
1088
    /**
1089
     * This function should return true if the current user can delete this page. It can be overloaded to customise the
1090
     * security model for an application.
1091
     *
1092
     * Denies permission if any of the following conditions is true:
1093
     * - canDelete() returns false on any extension
1094
     * - canEdit() returns false
1095
     * - any descendant page returns false for canDelete()
1096
     *
1097
     * @uses canDelete()
1098
     * @uses SiteTreeExtension->canDelete()
1099
     * @uses canEdit()
1100
     *
1101
     * @param Member $member
1102
     * @return bool True if the current user can delete this page
1103
     */
1104
    public function canDelete($member = null)
1105
    {
1106
        if (!$member) {
1107
            $member = Security::getCurrentUser();
1108
        }
1109
1110
        // Standard mechanism for accepting permission changes from extensions
1111
        $extended = $this->extendedCan('canDelete', $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...
1112
        if ($extended !== null) {
1113
            return $extended;
1114
        }
1115
1116
        if (!$member) {
1117
            return false;
1118
        }
1119
1120
        // Default permission check
1121
        if (Permission::checkMember($member, array("ADMIN", "SITETREE_EDIT_ALL"))) {
1122
            return true;
1123
        }
1124
1125
        // Check inherited permissions
1126
        return static::getPermissionChecker()
1127
            ->canDelete($this->ID, $member);
1128
    }
1129
1130
    /**
1131
     * This function should return true if the current user can create new pages of this class, regardless of class. It
1132
     * can be overloaded to customise the security model for an application.
1133
     *
1134
     * By default, permission to create at the root level is based on the SiteConfig configuration, and permission to
1135
     * create beneath a parent is based on the ability to edit that parent page.
1136
     *
1137
     * Use {@link canAddChildren()} to control behaviour of creating children under this page.
1138
     *
1139
     * @uses $can_create
1140
     * @uses DataExtension->canCreate()
1141
     *
1142
     * @param Member $member
1143
     * @param array $context Optional array which may contain array('Parent' => $parentObj)
1144
     *                       If a parent page is known, it will be checked for validity.
1145
     *                       If omitted, it will be assumed this is to be created as a top level page.
1146
     * @return bool True if the current user can create pages on this class.
1147
     */
1148
    public function canCreate($member = null, $context = array())
1149
    {
1150
        if (!$member) {
1151
            $member = Security::getCurrentUser();
1152
        }
1153
1154
        // Check parent (custom canCreate option for SiteTree)
1155
        // Block children not allowed for this parent type
1156
        $parent = isset($context['Parent']) ? $context['Parent'] : null;
1157
        if ($parent && !in_array(static::class, $parent->allowedChildren())) {
1158
            return false;
1159
        }
1160
1161
        // Standard mechanism for accepting permission changes from extensions
1162
        $extended = $this->extendedCan(__FUNCTION__, $member, $context);
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...
1163
        if ($extended !== null) {
1164
            return $extended;
1165
        }
1166
1167
        // Check permission
1168
        if ($member && Permission::checkMember($member, "ADMIN")) {
1169
            return true;
1170
        }
1171
1172
        // Fall over to inherited permissions
1173
        if ($parent && $parent->exists()) {
1174
            return $parent->canAddChildren($member);
1175
        } else {
1176
            // This doesn't necessarily mean we are creating a root page, but that
1177
            // we don't know if there is a parent, so default to this permission
1178
            return SiteConfig::current_site_config()->canCreateTopLevel($member);
1179
        }
1180
    }
1181
1182
    /**
1183
     * This function should return true if the current user can edit this page. It can be overloaded to customise the
1184
     * security model for an application.
1185
     *
1186
     * Denies permission if any of the following conditions is true:
1187
     * - canEdit() on any extension returns false
1188
     * - canView() return false
1189
     * - "CanEditType" directive is set to "Inherit" and any parent page return false for canEdit()
1190
     * - "CanEditType" directive is set to "LoggedInUsers" and no user is logged in or doesn't have the
1191
     *   CMS_Access_CMSMAIN permission code
1192
     * - "CanEditType" directive is set to "OnlyTheseUsers" and user is not in the given groups
1193
     *
1194
     * @uses canView()
1195
     * @uses EditorGroups()
1196
     * @uses DataExtension->canEdit()
1197
     *
1198
     * @param Member $member Set to false if you want to explicitly test permissions without a valid user (useful for
1199
     *                       unit tests)
1200
     * @return bool True if the current user can edit this page
1201
     */
1202 View Code Duplication
    public function canEdit($member = null)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in 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...
1203
    {
1204
        if (!$member) {
1205
            $member = Security::getCurrentUser();
1206
        }
1207
1208
        // Standard mechanism for accepting permission changes from extensions
1209
        $extended = $this->extendedCan('canEdit', $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...
1210
        if ($extended !== null) {
1211
            return $extended;
1212
        }
1213
1214
        // Default permissions
1215
        if (Permission::checkMember($member, "SITETREE_EDIT_ALL")) {
1216
            return true;
1217
        }
1218
1219
        // Check inherited permissions
1220
        return static::getPermissionChecker()
1221
            ->canEdit($this->ID, $member);
1222
    }
1223
1224
    /**
1225
     * Stub method to get the site config, unless the current class can provide an alternate.
1226
     *
1227
     * @return SiteConfig
1228
     */
1229
    public function getSiteConfig()
1230
    {
1231
        $configs = $this->invokeWithExtensions('alternateSiteConfig');
1232
        foreach (array_filter($configs) as $config) {
1233
            return $config;
1234
        }
1235
1236
        return SiteConfig::current_site_config();
1237
    }
1238
1239
    /**
1240
     * @return PermissionChecker
1241
     */
1242
    public static function getPermissionChecker()
1243
    {
1244
        return Injector::inst()->get(PermissionChecker::class.'.sitetree');
1245
    }
1246
1247
    /**
1248
     * Collate selected descendants of this page.
1249
     *
1250
     * {@link $condition} will be evaluated on each descendant, and if it is succeeds, that item will be added to the
1251
     * $collator array.
1252
     *
1253
     * @param string $condition The PHP condition to be evaluated. The page will be called $item
1254
     * @param array  $collator  An array, passed by reference, to collect all of the matching descendants.
1255
     * @return bool
1256
     */
1257
    public function collateDescendants($condition, &$collator)
1258
    {
1259
        // apply reasonable hierarchy limits
1260
        $threshold = Config::inst()->get(Hierarchy::class, 'node_threshold_leaf');
1261
        if ($this->numChildren() > $threshold) {
0 ignored issues
show
Documentation Bug introduced by
The method numChildren 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...
1262
            return false;
1263
        }
1264
1265
        $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...
1266
        if ($children) {
1267
            foreach ($children as $item) {
1268
                if (eval("return $condition;")) {
1269
                    $collator[] = $item;
1270
                }
1271
                /** @var SiteTree $item */
1272
                $item->collateDescendants($condition, $collator);
1273
            }
1274
            return true;
1275
        }
1276
        return false;
1277
    }
1278
1279
    /**
1280
     * Return the title, description, keywords and language metatags.
1281
     *
1282
     * @todo Move <title> tag in separate getter for easier customization and more obvious usage
1283
     *
1284
     * @param bool $includeTitle Show default <title>-tag, set to false for custom templating
1285
     * @return string The XHTML metatags
1286
     */
1287
    public function MetaTags($includeTitle = true)
1288
    {
1289
        $tags = array();
1290
        if ($includeTitle && strtolower($includeTitle) != 'false') {
1291
            $tags[] = HTML::createTag('title', array(), $this->obj('Title')->forTemplate());
1292
        }
1293
1294
        $generator = trim(Config::inst()->get(self::class, 'meta_generator'));
1295
        if (!empty($generator)) {
1296
            $tags[] = HTML::createTag('meta', array(
1297
                'name' => 'generator',
1298
                'content' => $generator,
1299
            ));
1300
        }
1301
1302
        $charset = ContentNegotiator::config()->uninherited('encoding');
1303
        $tags[] = HTML::createTag('meta', array(
1304
            'http-equiv' => 'Content-Type',
1305
            'content' => 'text/html; charset=' . $charset,
1306
        ));
1307
        if ($this->MetaDescription) {
1308
            $tags[] = HTML::createTag('meta', array(
1309
                'name' => 'description',
1310
                'content' => $this->MetaDescription,
1311
            ));
1312
        }
1313
1314
        if (Permission::check('CMS_ACCESS_CMSMain')
1315
            && $this->ID > 0
1316
        ) {
1317
            $tags[] = HTML::createTag('meta', array(
1318
                'name' => 'x-page-id',
1319
                'content' => $this->obj('ID')->forTemplate(),
1320
            ));
1321
            $tags[] = HTML::createTag('meta', array(
1322
                'name' => 'x-cms-edit-link',
1323
                'content' => $this->obj('CMSEditLink')->forTemplate(),
1324
            ));
1325
        }
1326
1327
        $tags = implode("\n", $tags);
1328
        if ($this->ExtraMeta) {
1329
            $tags .= $this->obj('ExtraMeta')->forTemplate();
1330
        }
1331
1332
        $this->extend('MetaTags', $tags);
1333
1334
        return $tags;
1335
    }
1336
1337
    /**
1338
     * Returns the object that contains the content that a user would associate with this page.
1339
     *
1340
     * Ordinarily, this is just the page itself, but for example on RedirectorPages or VirtualPages ContentSource() will
1341
     * return the page that is linked to.
1342
     *
1343
     * @return $this
1344
     */
1345
    public function ContentSource()
1346
    {
1347
        return $this;
1348
    }
1349
1350
    /**
1351
     * Add default records to database.
1352
     *
1353
     * This function is called whenever the database is built, after the database tables have all been created. Overload
1354
     * this to add default records when the database is built, but make sure you call parent::requireDefaultRecords().
1355
     */
1356
    public function requireDefaultRecords()
1357
    {
1358
        parent::requireDefaultRecords();
1359
1360
        // default pages
1361
        if (static::class == self::class && $this->config()->create_default_pages) {
1362
            if (!SiteTree::get_by_link(RootURLController::config()->default_homepage_link)) {
1363
                $homepage = new Page();
1364
                $homepage->Title = _t(__CLASS__.'.DEFAULTHOMETITLE', 'Home');
1365
                $homepage->Content = _t(__CLASS__.'.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>');
1366
                $homepage->URLSegment = RootURLController::config()->default_homepage_link;
1367
                $homepage->Sort = 1;
1368
                $homepage->write();
1369
                $homepage->copyVersionToStage(Versioned::DRAFT, Versioned::LIVE);
1370
                $homepage->flushCache();
1371
                DB::alteration_message('Home page created', 'created');
1372
            }
1373
1374
            if (DB::query("SELECT COUNT(*) FROM \"SiteTree\"")->value() == 1) {
1375
                $aboutus = new Page();
1376
                $aboutus->Title = _t(__CLASS__.'.DEFAULTABOUTTITLE', 'About Us');
1377
                $aboutus->Content = _t(
1378
                    'SilverStripe\\CMS\\Model\\SiteTree.DEFAULTABOUTCONTENT',
1379
                    '<p>You can fill this page out with your own content, or delete it and create your own pages.</p>'
1380
                );
1381
                $aboutus->Sort = 2;
1382
                $aboutus->write();
1383
                $aboutus->copyVersionToStage(Versioned::DRAFT, Versioned::LIVE);
1384
                $aboutus->flushCache();
1385
                DB::alteration_message('About Us page created', 'created');
1386
1387
                $contactus = new Page();
1388
                $contactus->Title = _t(__CLASS__.'.DEFAULTCONTACTTITLE', 'Contact Us');
1389
                $contactus->Content = _t(
1390
                    'SilverStripe\\CMS\\Model\\SiteTree.DEFAULTCONTACTCONTENT',
1391
                    '<p>You can fill this page out with your own content, or delete it and create your own pages.</p>'
1392
                );
1393
                $contactus->Sort = 3;
1394
                $contactus->write();
1395
                $contactus->copyVersionToStage(Versioned::DRAFT, Versioned::LIVE);
1396
                $contactus->flushCache();
1397
                DB::alteration_message('Contact Us page created', 'created');
1398
            }
1399
        }
1400
    }
1401
1402
    protected function onBeforeWrite()
1403
    {
1404
        parent::onBeforeWrite();
1405
1406
        // If Sort hasn't been set, make this page come after it's siblings
1407
        if (!$this->Sort) {
1408
            $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...
1409
            $this->Sort = DB::prepared_query(
1410
                "SELECT MAX(\"Sort\") + 1 FROM \"SiteTree\" WHERE \"ParentID\" = ?",
1411
                array($parentID)
1412
            )->value();
1413
        }
1414
1415
        // If there is no URLSegment set, generate one from Title
1416
        $defaultSegment = $this->generateURLSegment(_t(
1417
            'SilverStripe\\CMS\\Controllers\\CMSMain.NEWPAGE',
1418
            'New {pagetype}',
1419
            array('pagetype' => $this->i18n_singular_name())
1420
        ));
1421
        if ((!$this->URLSegment || $this->URLSegment == $defaultSegment) && $this->Title) {
1422
            $this->URLSegment = $this->generateURLSegment($this->Title);
1423
        } elseif ($this->isChanged('URLSegment', 2)) {
1424
            // Do a strict check on change level, to avoid double encoding caused by
1425
            // bogus changes through forceChange()
1426
            $filter = URLSegmentFilter::create();
1427
            $this->URLSegment = $filter->filter($this->URLSegment);
1428
            // If after sanitising there is no URLSegment, give it a reasonable default
1429
            if (!$this->URLSegment) {
1430
                $this->URLSegment = "page-$this->ID";
1431
            }
1432
        }
1433
1434
        // Ensure that this object has a non-conflicting URLSegment value.
1435
        $count = 2;
1436
        while (!$this->validURLSegment()) {
1437
            $this->URLSegment = preg_replace('/-[0-9]+$/', null, $this->URLSegment) . '-' . $count;
1438
            $count++;
1439
        }
1440
1441
        $this->syncLinkTracking();
1442
1443
        // Check to see if we've only altered fields that shouldn't affect versioning
1444
        $fieldsIgnoredByVersioning = array('HasBrokenLink', 'Status', 'HasBrokenFile', 'ToDo', 'VersionID', 'SaveCount');
1445
        $changedFields = array_keys($this->getChangedFields(true, 2));
1446
1447
        // This more rigorous check is inline with the test that write() does to decide whether or not to write to the
1448
        // DB. We use that to avoid cluttering the system with a migrateVersion() call that doesn't get used
1449
        $oneChangedFields = array_keys($this->getChangedFields(true, 1));
1450
1451
        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...
1452
            // This will have the affect of preserving the versioning
1453
            $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...
1454
        }
1455
    }
1456
1457
    /**
1458
     * Trigger synchronisation of link tracking
1459
     *
1460
     * {@see SiteTreeLinkTracking::augmentSyncLinkTracking}
1461
     */
1462
    public function syncLinkTracking()
1463
    {
1464
        $this->extend('augmentSyncLinkTracking');
1465
    }
1466
1467
    public function onBeforeDelete()
1468
    {
1469
        parent::onBeforeDelete();
1470
1471
        // If deleting this page, delete all its children.
1472
        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...
1473
            foreach ($children as $child) {
1474
                /** @var SiteTree $child */
1475
                $child->delete();
1476
            }
1477
        }
1478
    }
1479
1480
    public function onAfterDelete()
1481
    {
1482
        $this->updateDependentPages();
1483
        parent::onAfterDelete();
1484
    }
1485
1486
    public function flushCache($persistent = true)
1487
    {
1488
        parent::flushCache($persistent);
1489
        $this->_cache_statusFlags = null;
1490
    }
1491
1492
    public function validate()
1493
    {
1494
        $result = parent::validate();
1495
1496
        // Allowed children validation
1497
        $parent = $this->getParent();
1498
        if ($parent && $parent->exists()) {
1499
            // No need to check for subclasses or instanceof, as allowedChildren() already
1500
            // deconstructs any inheritance trees already.
1501
            $allowed = $parent->allowedChildren();
1502
            $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...
1503
                ? $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...
1504
                : $this;
1505
            if (!in_array($subject->ClassName, $allowed)) {
1506
                $result->addError(
1507
                    _t(
1508
                        'SilverStripe\\CMS\\Model\\SiteTree.PageTypeNotAllowed',
1509
                        'Page type "{type}" not allowed as child of this parent page',
1510
                        array('type' => $subject->i18n_singular_name())
1511
                    ),
1512
                    ValidationResult::TYPE_ERROR,
1513
                    'ALLOWED_CHILDREN'
1514
                );
1515
            }
1516
        }
1517
1518
        // "Can be root" validation
1519
        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...
1520
            $result->addError(
1521
                _t(
1522
                    'SilverStripe\\CMS\\Model\\SiteTree.PageTypNotAllowedOnRoot',
1523
                    'Page type "{type}" is not allowed on the root level',
1524
                    array('type' => $this->i18n_singular_name())
1525
                ),
1526
                ValidationResult::TYPE_ERROR,
1527
                'CAN_BE_ROOT'
1528
            );
1529
        }
1530
1531
        return $result;
1532
    }
1533
1534
    /**
1535
     * Returns true if this object has a URLSegment value that does not conflict with any other objects. This method
1536
     * checks for:
1537
     *  - A page with the same URLSegment that has a conflict
1538
     *  - Conflicts with actions on the parent page
1539
     *  - A conflict caused by a root page having the same URLSegment as a class name
1540
     *
1541
     * @return bool
1542
     */
1543
    public function validURLSegment()
1544
    {
1545
        if (self::config()->nested_urls && $parent = $this->Parent()) {
1546
            if ($controller = ModelAsController::controller_for($parent)) {
1547
                if ($controller instanceof Controller && $controller->hasAction($this->URLSegment)) {
1548
                    return false;
1549
                }
1550
            }
1551
        }
1552
1553
        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...
1554
            if (class_exists($this->URLSegment) && is_subclass_of($this->URLSegment, RequestHandler::class)) {
0 ignored issues
show
Bug introduced by
Due to PHP Bug #53727, is_subclass_of might return inconsistent results on some PHP versions if \SilverStripe\Control\RequestHandler::class can be an interface. If so, you could instead use ReflectionClass::implementsInterface.
Loading history...
1555
                return false;
1556
            }
1557
        }
1558
1559
        // Filters by url, id, and parent
1560
        $filter = array('"SiteTree"."URLSegment"' => $this->URLSegment);
1561
        if ($this->ID) {
1562
            $filter['"SiteTree"."ID" <> ?'] = $this->ID;
1563
        }
1564
        if (self::config()->nested_urls) {
1565
            $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...
1566
        }
1567
1568
        // If any of the extensions return `0` consider the segment invalid
1569
        $extensionResponses = array_filter(
1570
            (array)$this->extend('augmentValidURLSegment'),
1571
            function ($response) {
1572
                return !is_null($response);
1573
            }
1574
        );
1575
        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...
1576
            return min($extensionResponses);
1577
        }
1578
1579
        // Check existence
1580
        return !DataObject::get(self::class, $filter)->exists();
1581
    }
1582
1583
    /**
1584
     * Generate a URL segment based on the title provided.
1585
     *
1586
     * If {@link Extension}s wish to alter URL segment generation, they can do so by defining
1587
     * updateURLSegment(&$url, $title).  $url will be passed by reference and should be modified. $title will contain
1588
     * the title that was originally used as the source of this generated URL. This lets extensions either start from
1589
     * scratch, or incrementally modify the generated URL.
1590
     *
1591
     * @param string $title Page title
1592
     * @return string Generated url segment
1593
     */
1594
    public function generateURLSegment($title)
1595
    {
1596
        $filter = URLSegmentFilter::create();
1597
        $t = $filter->filter($title);
1598
1599
        // Fallback to generic page name if path is empty (= no valid, convertable characters)
1600
        if (!$t || $t == '-' || $t == '-1') {
1601
            $t = "page-$this->ID";
1602
        }
1603
1604
        // Hook for extensions
1605
        $this->extend('updateURLSegment', $t, $title);
1606
1607
        return $t;
1608
    }
1609
1610
    /**
1611
     * Gets the URL segment for the latest draft version of this page.
1612
     *
1613
     * @return string
1614
     */
1615
    public function getStageURLSegment()
1616
    {
1617
        $stageRecord = Versioned::get_one_by_stage(self::class, Versioned::DRAFT, array(
1618
            '"SiteTree"."ID"' => $this->ID
1619
        ));
1620
        return ($stageRecord) ? $stageRecord->URLSegment : null;
1621
    }
1622
1623
    /**
1624
     * Gets the URL segment for the currently published version of this page.
1625
     *
1626
     * @return string
1627
     */
1628
    public function getLiveURLSegment()
1629
    {
1630
        $liveRecord = Versioned::get_one_by_stage(self::class, Versioned::LIVE, array(
1631
            '"SiteTree"."ID"' => $this->ID
1632
        ));
1633
        return ($liveRecord) ? $liveRecord->URLSegment : null;
1634
    }
1635
1636
    /**
1637
     * Returns the pages that depend on this page. This includes virtual pages, pages that link to it, etc.
1638
     *
1639
     * @param bool $includeVirtuals Set to false to exlcude virtual pages.
1640
     * @return ArrayList
1641
     */
1642
    public function DependentPages($includeVirtuals = true)
1643
    {
1644
        if (class_exists('Subsite')) {
1645
            $origDisableSubsiteFilter = Subsite::$disable_subsite_filter;
1646
            Subsite::disable_subsite_filter(true);
1647
        }
1648
1649
        // Content links
1650
        $items = new ArrayList();
1651
1652
        // We merge all into a regular SS_List, because DataList doesn't support merge
1653
        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...
1654
            $linkList = new ArrayList();
1655
            foreach ($contentLinks as $item) {
1656
                $item->DependentLinkType = 'Content link';
1657
                $linkList->push($item);
1658
            }
1659
            $items->merge($linkList);
1660
        }
1661
1662
        // Virtual pages
1663
        if ($includeVirtuals) {
1664
            $virtuals = $this->VirtualPages();
1665
            if ($virtuals) {
1666
                $virtualList = new ArrayList();
1667
                foreach ($virtuals as $item) {
1668
                    $item->DependentLinkType = 'Virtual page';
1669
                    $virtualList->push($item);
1670
                }
1671
                $items->merge($virtualList);
1672
            }
1673
        }
1674
1675
        // Redirector pages
1676
        $redirectors = RedirectorPage::get()->where(array(
1677
            '"RedirectorPage"."RedirectionType"' => 'Internal',
1678
            '"RedirectorPage"."LinkToID"' => $this->ID
1679
        ));
1680
        if ($redirectors) {
1681
            $redirectorList = new ArrayList();
1682
            foreach ($redirectors as $item) {
1683
                $item->DependentLinkType = 'Redirector page';
1684
                $redirectorList->push($item);
1685
            }
1686
            $items->merge($redirectorList);
1687
        }
1688
1689
        if (class_exists('Subsite')) {
1690
            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...
1691
        }
1692
1693
        return $items;
1694
    }
1695
1696
    /**
1697
     * Return all virtual pages that link to this page.
1698
     *
1699
     * @return DataList
1700
     */
1701
    public function VirtualPages()
1702
    {
1703
        $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...
1704
1705
        // Disable subsite filter for these pages
1706
        if ($pages instanceof DataList) {
1707
            return $pages->setDataQueryParam('Subsite.filter', false);
1708
        } else {
1709
            return $pages;
1710
        }
1711
    }
1712
1713
    /**
1714
     * Returns a FieldList with which to create the main editing form.
1715
     *
1716
     * You can override this in your child classes to add extra fields - first get the parent fields using
1717
     * parent::getCMSFields(), then use addFieldToTab() on the FieldList.
1718
     *
1719
     * See {@link getSettingsFields()} for a different set of fields concerned with configuration aspects on the record,
1720
     * e.g. access control.
1721
     *
1722
     * @return FieldList The fields to be displayed in the CMS
1723
     */
1724
    public function getCMSFields()
1725
    {
1726
        // Status / message
1727
        // Create a status message for multiple parents
1728
        if ($this->ID && is_numeric($this->ID)) {
1729
            $linkedPages = $this->VirtualPages();
1730
1731
            $parentPageLinks = array();
1732
1733
            if ($linkedPages->count() > 0) {
1734
                /** @var VirtualPage $linkedPage */
1735
                foreach ($linkedPages as $linkedPage) {
1736
                    $parentPage = $linkedPage->Parent();
1737
                    if ($parentPage && $parentPage->exists()) {
1738
                        $link = Convert::raw2att($parentPage->CMSEditLink());
1739
                        $title = Convert::raw2xml($parentPage->Title);
1740
                    } else {
1741
                        $link = CMSPageEditController::singleton()->Link('show');
1742
                        $title = _t(__CLASS__.'.TOPLEVEL', 'Site Content (Top Level)');
1743
                    }
1744
                    $parentPageLinks[] = "<a class=\"cmsEditlink\" href=\"{$link}\">{$title}</a>";
1745
                }
1746
1747
                $lastParent = array_pop($parentPageLinks);
1748
                $parentList = "'$lastParent'";
1749
1750
                if (count($parentPageLinks)) {
1751
                    $parentList = "'" . implode("', '", $parentPageLinks) . "' and "
1752
                        . $parentList;
1753
                }
1754
1755
                $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...
1756
                    'SilverStripe\\CMS\\Model\\SiteTree.APPEARSVIRTUALPAGES',
1757
                    "This content also appears on the virtual pages in the {title} sections.",
1758
                    array('title' => $parentList)
1759
                );
1760
            }
1761
        }
1762
1763
        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...
1764
            $statusMessage[] = _t(__CLASS__.'.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...
1765
        }
1766
1767
        $dependentNote = '';
1768
        $dependentTable = new LiteralField('DependentNote', '<p></p>');
1769
1770
        // Create a table for showing pages linked to this one
1771
        $dependentPages = $this->DependentPages();
1772
        $dependentPagesCount = $dependentPages->count();
1773
        if ($dependentPagesCount) {
1774
            $dependentColumns = array(
1775
                'Title' => $this->fieldLabel('Title'),
1776
                'AbsoluteLink' => _t(__CLASS__.'.DependtPageColumnURL', 'URL'),
1777
                'DependentLinkType' => _t(__CLASS__.'.DependtPageColumnLinkType', 'Link type'),
1778
            );
1779
            if (class_exists('Subsite')) {
1780
                $dependentColumns['Subsite.Title'] = singleton('Subsite')->i18n_singular_name();
1781
            }
1782
1783
            $dependentNote = new LiteralField('DependentNote', '<p>' . _t(__CLASS__.'.DEPENDENT_NOTE', 'The following pages depend on this page. This includes virtual pages, redirector pages, and pages with content links.') . '</p>');
1784
            $dependentTable = GridField::create(
1785
                'DependentPages',
1786
                false,
1787
                $dependentPages
1788
            );
1789
            /** @var GridFieldDataColumns $dataColumns */
1790
            $dataColumns = $dependentTable->getConfig()->getComponentByType('SilverStripe\\Forms\\GridField\\GridFieldDataColumns');
1791
            $dataColumns
1792
                ->setDisplayFields($dependentColumns)
1793
                ->setFieldFormatting(array(
1794
                    'Title' => function ($value, &$item) {
1795
                        return sprintf(
1796
                            '<a href="admin/pages/edit/show/%d">%s</a>',
1797
                            (int)$item->ID,
1798
                            Convert::raw2xml($item->Title)
1799
                        );
1800
                    },
1801
                    '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...
1802
                        return sprintf(
1803
                            '<a href="%s" target="_blank">%s</a>',
1804
                            Convert::raw2xml($value),
1805
                            Convert::raw2xml($value)
1806
                        );
1807
                    }
1808
                ));
1809
        }
1810
1811
        $baseLink = Controller::join_links(
1812
            Director::absoluteBaseURL(),
1813
            (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...
1814
        );
1815
1816
        $urlsegment = SiteTreeURLSegmentField::create("URLSegment", $this->fieldLabel('URLSegment'))
1817
            ->setURLPrefix($baseLink)
1818
            ->setDefaultURL($this->generateURLSegment(_t(
1819
                'SilverStripe\\CMS\\Controllers\\CMSMain.NEWPAGE',
1820
                'New {pagetype}',
1821
                array('pagetype' => $this->i18n_singular_name())
1822
            )));
1823
        $helpText = (self::config()->nested_urls && $this->numChildren())
0 ignored issues
show
Documentation Bug introduced by
The method numChildren 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...
1824
            ? $this->fieldLabel('LinkChangeNote')
1825
            : '';
1826
        if (!Config::inst()->get('SilverStripe\\View\\Parsers\\URLSegmentFilter', 'default_allow_multibyte')) {
1827
            $helpText .= _t('SilverStripe\\CMS\\Forms\\SiteTreeURLSegmentField.HelpChars', ' Special characters are automatically converted or removed.');
1828
        }
1829
        $urlsegment->setHelpText($helpText);
1830
1831
        $fields = new FieldList(
1832
            $rootTab = new TabSet(
1833
                "Root",
1834
                $tabMain = new Tab(
1835
                    'Main',
1836
                    new TextField("Title", $this->fieldLabel('Title')),
1837
                    $urlsegment,
1838
                    new TextField("MenuTitle", $this->fieldLabel('MenuTitle')),
1839
                    $htmlField = new HTMLEditorField("Content", _t(__CLASS__.'.HTMLEDITORTITLE', "Content", 'HTML editor title')),
1840
                    ToggleCompositeField::create(
1841
                        'Metadata',
1842
                        _t(__CLASS__.'.MetadataToggle', 'Metadata'),
1843
                        array(
1844
                            $metaFieldDesc = new TextareaField("MetaDescription", $this->fieldLabel('MetaDescription')),
1845
                            $metaFieldExtra = new TextareaField("ExtraMeta", $this->fieldLabel('ExtraMeta'))
1846
                        )
1847
                    )->setHeadingLevel(4)
1848
                ),
1849
                $tabDependent = new Tab(
1850
                    'Dependent',
1851
                    $dependentNote,
1852
                    $dependentTable
1853
                )
1854
            )
1855
        );
1856
        $htmlField->addExtraClass('stacked');
1857
1858
        // Help text for MetaData on page content editor
1859
        $metaFieldDesc
1860
            ->setRightTitle(
1861
                _t(
1862
                    'SilverStripe\\CMS\\Model\\SiteTree.METADESCHELP',
1863
                    "Search engines use this content for displaying search results (although it will not influence their ranking)."
1864
                )
1865
            )
1866
            ->addExtraClass('help');
1867
        $metaFieldExtra
1868
            ->setRightTitle(
1869
                _t(
1870
                    'SilverStripe\\CMS\\Model\\SiteTree.METAEXTRAHELP',
1871
                    "HTML tags for additional meta information. For example &lt;meta name=\"customName\" content=\"your custom content here\" /&gt;"
1872
                )
1873
            )
1874
            ->addExtraClass('help');
1875
1876
        // Conditional dependent pages tab
1877
        if ($dependentPagesCount) {
1878
            $tabDependent->setTitle(_t(__CLASS__.'.TABDEPENDENT', "Dependent pages") . " ($dependentPagesCount)");
1879
        } else {
1880
            $fields->removeFieldFromTab('Root', 'Dependent');
1881
        }
1882
1883
        $tabMain->setTitle(_t(__CLASS__.'.TABCONTENT', "Main Content"));
1884
1885
        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...
1886
            $obsoleteWarning = _t(
1887
                'SilverStripe\\CMS\\Model\\SiteTree.OBSOLETECLASS',
1888
                "This page is of obsolete type {type}. Saving will reset its type and you may lose data",
1889
                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...
1890
            );
1891
1892
            $fields->addFieldToTab(
1893
                "Root.Main",
1894
                new LiteralField("ObsoleteWarningHeader", "<p class=\"message warning\">$obsoleteWarning</p>"),
1895
                "Title"
1896
            );
1897
        }
1898
1899
        if (file_exists(BASE_PATH . '/install.php')) {
1900
            $fields->addFieldToTab("Root.Main", new LiteralField(
1901
                "InstallWarningHeader",
1902
                "<p class=\"message warning\">" . _t(
1903
                    "SilverStripe\\CMS\\Model\\SiteTree.REMOVE_INSTALL_WARNING",
1904
                    "Warning: You should remove install.php from this SilverStripe install for security reasons."
1905
                )
1906
                . "</p>"
1907
            ), "Title");
1908
        }
1909
1910
        if (self::$runCMSFieldsExtensions) {
1911
            $this->extend('updateCMSFields', $fields);
1912
        }
1913
1914
        return $fields;
1915
    }
1916
1917
1918
    /**
1919
     * Returns fields related to configuration aspects on this record, e.g. access control. See {@link getCMSFields()}
1920
     * for content-related fields.
1921
     *
1922
     * @return FieldList
1923
     */
1924
    public function getSettingsFields()
1925
    {
1926
        $mapFn = function ($groups = []) {
1927
            $map = [];
1928
            foreach ($groups as $group) {
1929
                // Listboxfield values are escaped, use ASCII char instead of &raquo;
1930
                $map[$group->ID] = $group->getBreadcrumbs(' > ');
1931
            }
1932
            asort($map);
1933
            return $map;
1934
        };
1935
        $groupsMap = $mapFn(Group::get());
1936
        $viewAllGroupsMap = $mapFn(Permission::get_groups_by_permission(['SITETREE_VIEW_ALL', 'ADMIN']));
1937
        $editAllGroupsMap = $mapFn(Permission::get_groups_by_permission(['SITETREE_EDIT_ALL', 'ADMIN']));
1938
1939
        $fields = new FieldList(
1940
            $rootTab = new TabSet(
1941
                "Root",
1942
                $tabBehaviour = new Tab(
1943
                    'Settings',
1944
                    new DropdownField(
1945
                        "ClassName",
1946
                        $this->fieldLabel('ClassName'),
1947
                        $this->getClassDropdown()
1948
                    ),
1949
                    $parentTypeSelector = new CompositeField(
1950
                        $parentType = new OptionsetField("ParentType", _t("SilverStripe\\CMS\\Model\\SiteTree.PAGELOCATION", "Page location"), array(
1951
                            "root" => _t("SilverStripe\\CMS\\Model\\SiteTree.PARENTTYPE_ROOT", "Top-level page"),
1952
                            "subpage" => _t("SilverStripe\\CMS\\Model\\SiteTree.PARENTTYPE_SUBPAGE", "Sub-page underneath a parent page"),
1953
                        )),
1954
                        $parentIDField = new TreeDropdownField("ParentID", $this->fieldLabel('ParentID'), self::class, 'ID', 'MenuTitle')
1955
                    ),
1956
                    $visibility = new FieldGroup(
1957
                        new CheckboxField("ShowInMenus", $this->fieldLabel('ShowInMenus')),
1958
                        new CheckboxField("ShowInSearch", $this->fieldLabel('ShowInSearch'))
1959
                    ),
1960
                    $viewersOptionsField = new OptionsetField(
1961
                        "CanViewType",
1962
                        _t(__CLASS__.'.ACCESSHEADER', "Who can view this page?")
1963
                    ),
1964
                    $viewerGroupsField = ListboxField::create("ViewerGroups", _t(__CLASS__.'.VIEWERGROUPS', "Viewer Groups"))
1965
                        ->setSource($groupsMap)
1966
                        ->setAttribute(
1967
                            'data-placeholder',
1968
                            _t(__CLASS__.'.GroupPlaceholder', 'Click to select group')
1969
                        ),
1970
                    $editorsOptionsField = new OptionsetField(
1971
                        "CanEditType",
1972
                        _t(__CLASS__.'.EDITHEADER', "Who can edit this page?")
1973
                    ),
1974
                    $editorGroupsField = ListboxField::create("EditorGroups", _t(__CLASS__.'.EDITORGROUPS', "Editor Groups"))
1975
                        ->setSource($groupsMap)
1976
                        ->setAttribute(
1977
                            'data-placeholder',
1978
                            _t(__CLASS__.'.GroupPlaceholder', 'Click to select group')
1979
                        )
1980
                )
1981
            )
1982
        );
1983
1984
        $parentType->addExtraClass('noborder');
1985
        $visibility->setTitle($this->fieldLabel('Visibility'));
1986
1987
1988
        // This filter ensures that the ParentID dropdown selection does not show this node,
1989
        // or its descendents, as this causes vanishing bugs
1990
        $parentIDField->setFilterFunction(function ($node) {
1991
            return $node->ID != $this->ID;
1992
        });
1993
        $parentTypeSelector->addExtraClass('parentTypeSelector');
1994
1995
        $tabBehaviour->setTitle(_t(__CLASS__.'.TABBEHAVIOUR', "Behavior"));
1996
1997
        // Make page location fields read-only if the user doesn't have the appropriate permission
1998
        if (!Permission::check("SITETREE_REORGANISE")) {
1999
            $fields->makeFieldReadonly('ParentType');
2000
            if ($this->getParentType() === 'root') {
2001
                $fields->removeByName('ParentID');
2002
            } else {
2003
                $fields->makeFieldReadonly('ParentID');
2004
            }
2005
        }
2006
2007
        $viewersOptionsSource = [
2008
            InheritedPermissions::INHERIT => _t(__CLASS__.'.INHERIT', "Inherit from parent page"),
2009
            InheritedPermissions::ANYONE => _t(__CLASS__.'.ACCESSANYONE', "Anyone"),
2010
            InheritedPermissions::LOGGED_IN_USERS => _t(__CLASS__.'.ACCESSLOGGEDIN', "Logged-in users"),
2011
            InheritedPermissions::ONLY_THESE_USERS => _t(
2012
                __CLASS__.'.ACCESSONLYTHESE',
2013
                "Only these people (choose from list)"
2014
            ),
2015
        ];
2016
        $viewersOptionsField->setSource($viewersOptionsSource);
2017
2018
        // Editors have same options, except no "Anyone"
2019
        $editorsOptionsSource = $viewersOptionsSource;
2020
        unset($editorsOptionsSource[InheritedPermissions::ANYONE]);
2021
        $editorsOptionsField->setSource($editorsOptionsSource);
2022
2023
        if ($viewAllGroupsMap) {
2024
            $viewerGroupsField->setDescription(_t(
2025
                'SilverStripe\\CMS\\Model\\SiteTree.VIEWER_GROUPS_FIELD_DESC',
2026
                'Groups with global view permissions: {groupList}',
2027
                ['groupList' => implode(', ', array_values($viewAllGroupsMap))]
2028
            ));
2029
        }
2030
2031
        if ($editAllGroupsMap) {
2032
            $editorGroupsField->setDescription(_t(
2033
                'SilverStripe\\CMS\\Model\\SiteTree.EDITOR_GROUPS_FIELD_DESC',
2034
                'Groups with global edit permissions: {groupList}',
2035
                ['groupList' => implode(', ', array_values($editAllGroupsMap))]
2036
            ));
2037
        }
2038
2039
        if (!Permission::check('SITETREE_GRANT_ACCESS')) {
2040
            $fields->makeFieldReadonly($viewersOptionsField);
2041
            if ($this->CanEditType === InheritedPermissions::ONLY_THESE_USERS) {
0 ignored issues
show
Documentation introduced by
The property CanEditType 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...
2042
                $fields->makeFieldReadonly($viewerGroupsField);
2043
            } else {
2044
                $fields->removeByName('ViewerGroups');
2045
            }
2046
2047
            $fields->makeFieldReadonly($editorsOptionsField);
2048
            if ($this->CanEditType === InheritedPermissions::ONLY_THESE_USERS) {
0 ignored issues
show
Documentation introduced by
The property CanEditType 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...
2049
                $fields->makeFieldReadonly($editorGroupsField);
2050
            } else {
2051
                $fields->removeByName('EditorGroups');
2052
            }
2053
        }
2054
2055
        if (self::$runCMSFieldsExtensions) {
2056
            $this->extend('updateSettingsFields', $fields);
2057
        }
2058
2059
        return $fields;
2060
    }
2061
2062
    /**
2063
     * @param bool $includerelations A boolean value to indicate if the labels returned should include relation fields
2064
     * @return array
2065
     */
2066
    public function fieldLabels($includerelations = true)
2067
    {
2068
        $cacheKey = static::class . '_' . $includerelations;
2069
        if (!isset(self::$_cache_field_labels[$cacheKey])) {
2070
            $labels = parent::fieldLabels($includerelations);
2071
            $labels['Title'] = _t(__CLASS__.'.PAGETITLE', "Page name");
2072
            $labels['MenuTitle'] = _t(__CLASS__.'.MENUTITLE', "Navigation label");
2073
            $labels['MetaDescription'] = _t(__CLASS__.'.METADESC', "Meta Description");
2074
            $labels['ExtraMeta'] = _t(__CLASS__.'.METAEXTRA', "Custom Meta Tags");
2075
            $labels['ClassName'] = _t(__CLASS__.'.PAGETYPE', "Page type", 'Classname of a page object');
2076
            $labels['ParentType'] = _t(__CLASS__.'.PARENTTYPE', "Page location");
2077
            $labels['ParentID'] = _t(__CLASS__.'.PARENTID', "Parent page");
2078
            $labels['ShowInMenus'] =_t(__CLASS__.'.SHOWINMENUS', "Show in menus?");
2079
            $labels['ShowInSearch'] = _t(__CLASS__.'.SHOWINSEARCH', "Show in search?");
2080
            $labels['ViewerGroups'] = _t(__CLASS__.'.VIEWERGROUPS', "Viewer Groups");
2081
            $labels['EditorGroups'] = _t(__CLASS__.'.EDITORGROUPS', "Editor Groups");
2082
            $labels['URLSegment'] = _t(__CLASS__.'.URLSegment', 'URL Segment', 'URL for this page');
2083
            $labels['Content'] = _t(__CLASS__.'.Content', 'Content', 'Main HTML Content for a page');
2084
            $labels['CanViewType'] = _t(__CLASS__.'.Viewers', 'Viewers Groups');
2085
            $labels['CanEditType'] = _t(__CLASS__.'.Editors', 'Editors Groups');
2086
            $labels['Comments'] = _t(__CLASS__.'.Comments', 'Comments');
2087
            $labels['Visibility'] = _t(__CLASS__.'.Visibility', 'Visibility');
2088
            $labels['LinkChangeNote'] = _t(
2089
                'SilverStripe\\CMS\\Model\\SiteTree.LINKCHANGENOTE',
2090
                'Changing this page\'s link will also affect the links of all child pages.'
2091
            );
2092
2093
            if ($includerelations) {
2094
                $labels['Parent'] = _t(__CLASS__.'.has_one_Parent', 'Parent Page', 'The parent page in the site hierarchy');
2095
                $labels['LinkTracking'] = _t(__CLASS__.'.many_many_LinkTracking', 'Link Tracking');
2096
                $labels['ImageTracking'] = _t(__CLASS__.'.many_many_ImageTracking', 'Image Tracking');
2097
                $labels['BackLinkTracking'] = _t(__CLASS__.'.many_many_BackLinkTracking', 'Backlink Tracking');
2098
            }
2099
2100
            self::$_cache_field_labels[$cacheKey] = $labels;
2101
        }
2102
2103
        return self::$_cache_field_labels[$cacheKey];
2104
    }
2105
2106
    /**
2107
     * Get the actions available in the CMS for this page - eg Save, Publish.
2108
     *
2109
     * Frontend scripts and styles know how to handle the following FormFields:
2110
     * - top-level FormActions appear as standalone buttons
2111
     * - top-level CompositeField with FormActions within appear as grouped buttons
2112
     * - TabSet & Tabs appear as a drop ups
2113
     * - FormActions within the Tab are restyled as links
2114
     * - major actions can provide alternate states for richer presentation (see ssui.button widget extension)
2115
     *
2116
     * @return FieldList The available actions for this page.
2117
     */
2118
    public function getCMSActions()
2119
    {
2120
        // Get status of page
2121
        $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...
2122
        $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...
2123
        $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...
2124
2125
        // Check permissions
2126
        $canPublish = $this->canPublish();
2127
        $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...
2128
        $canEdit = $this->canEdit();
2129
2130
        // Major actions appear as buttons immediately visible as page actions.
2131
        $majorActions = CompositeField::create()->setName('MajorActions');
2132
        $majorActions->setFieldHolderTemplate(get_class($majorActions) . '_holder_buttongroup');
2133
2134
        // Minor options are hidden behind a drop-up and appear as links (although they are still FormActions).
2135
        $rootTabSet = new TabSet('ActionMenus');
2136
        $moreOptions = new Tab(
2137
            'MoreOptions',
2138
            _t(__CLASS__.'.MoreOptions', 'More options', 'Expands a view for more buttons')
2139
        );
2140
        $moreOptions->addExtraClass('popover-actions-simulate');
2141
        $rootTabSet->push($moreOptions);
2142
        $rootTabSet->addExtraClass('ss-ui-action-tabset action-menus noborder');
2143
2144
        // Render page information into the "more-options" drop-up, on the top.
2145
        $liveRecord = Versioned::get_by_stage(self::class, Versioned::LIVE)->byID($this->ID);
2146
        $infoTemplate = SSViewer::get_templates_by_class(static::class, '_Information', self::class);
2147
        $moreOptions->push(
2148
            new LiteralField(
2149
                'Information',
2150
                $this->customise(array(
2151
                    'Live' => $liveRecord,
2152
                    'ExistsOnLive' => $isPublished
2153
                ))->renderWith($infoTemplate)
2154
            )
2155
        );
2156
2157
        // Add to campaign option if not-archived and has publish permission
2158
        if (($isPublished || $isOnDraft) && $canPublish) {
2159
            $moreOptions->push(
2160
                AddToCampaignHandler_FormAction::create()
2161
                    ->removeExtraClass('btn-primary')
2162
                    ->addExtraClass('btn-secondary')
2163
            );
2164
        }
2165
2166
        // "readonly"/viewing version that isn't the current version of the record
2167
        $stageRecord = Versioned::get_by_stage(static::class, Versioned::DRAFT)->byID($this->ID);
2168
        /** @skipUpgrade */
2169
        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...
2170
            $moreOptions->push(FormAction::create('email', _t('SilverStripe\\CMS\\Controllers\\CMSMain.EMAIL', 'Email')));
2171
            $moreOptions->push(FormAction::create('rollback', _t('SilverStripe\\CMS\\Controllers\\CMSMain.ROLLBACK', 'Roll back to this version')));
2172
            $actions = new FieldList(array($majorActions, $rootTabSet));
2173
2174
            // getCMSActions() can be extended with updateCMSActions() on a extension
2175
            $this->extend('updateCMSActions', $actions);
2176
            return $actions;
2177
        }
2178
2179
        // "unpublish"
2180 View Code Duplication
        if ($isPublished && $canPublish && $isOnDraft && $canUnpublish) {
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...
2181
            $moreOptions->push(
2182
                FormAction::create('unpublish', _t(__CLASS__.'.BUTTONUNPUBLISH', 'Unpublish'), 'delete')
2183
                    ->setDescription(_t(__CLASS__.'.BUTTONUNPUBLISHDESC', 'Remove this page from the published site'))
2184
                    ->addExtraClass('btn-secondary')
2185
            );
2186
        }
2187
2188
        // "rollback"
2189 View Code Duplication
        if ($isOnDraft && $isPublished && $canEdit && $stagesDiffer) {
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...
2190
            $moreOptions->push(
2191
                FormAction::create('rollback', _t(__CLASS__.'.BUTTONCANCELDRAFT', 'Cancel draft changes'))
2192
                    ->setDescription(_t(
2193
                        'SilverStripe\\CMS\\Model\\SiteTree.BUTTONCANCELDRAFTDESC',
2194
                        'Delete your draft and revert to the currently published page'
2195
                    ))
2196
                    ->addExtraClass('btn-secondary')
2197
            );
2198
        }
2199
2200
        // "restore"
2201
        if ($canEdit && !$isOnDraft && $isPublished) {
2202
            $majorActions->push(FormAction::create('revert', _t('SilverStripe\\CMS\\Controllers\\CMSMain.RESTORE', 'Restore')));
2203
        }
2204
2205
        // Check if we can restore a deleted page
2206
        // Note: It would be nice to have a canRestore() permission at some point
2207
        if ($canEdit && !$isOnDraft && !$isPublished) {
2208
            // Determine if we should force a restore to root (where once it was a subpage)
2209
            $restoreToRoot = $this->isParentArchived();
2210
2211
            // "restore"
2212
            $title = $restoreToRoot
2213
                ? _t('SilverStripe\\CMS\\Controllers\\CMSMain.RESTORE_TO_ROOT', 'Restore draft at top level')
2214
                : _t('SilverStripe\\CMS\\Controllers\\CMSMain.RESTORE', 'Restore draft');
2215
            $description = $restoreToRoot
2216
                ? _t('SilverStripe\\CMS\\Controllers\\CMSMain.RESTORE_TO_ROOT_DESC', 'Restore the archived version to draft as a top level page')
2217
                : _t('SilverStripe\\CMS\\Controllers\\CMSMain.RESTORE_DESC', 'Restore the archived version to draft');
2218
            $majorActions->push(
2219
                FormAction::create('restore', $title)
2220
                    ->setDescription($description)
2221
                    ->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...
2222
                    ->setAttribute('data-icon', 'decline')
2223
            );
2224
        }
2225
2226
        // If a page is on any stage it can be archived
2227
        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...
2228
            $title = $isPublished
2229
                ? _t('SilverStripe\\CMS\\Controllers\\CMSMain.UNPUBLISH_AND_ARCHIVE', 'Unpublish and archive')
2230
                : _t('SilverStripe\\CMS\\Controllers\\CMSMain.ARCHIVE', 'Archive');
2231
            $moreOptions->push(
2232
                FormAction::create('archive', $title)
2233
                    ->addExtraClass('delete btn btn-secondary')
2234
                    ->setDescription(_t(
2235
                        'SilverStripe\\CMS\\Model\\SiteTree.BUTTONDELETEDESC',
2236
                        'Remove from draft/live and send to archive'
2237
                    ))
2238
            );
2239
        }
2240
2241
        // "save", supports an alternate state that is still clickable, but notifies the user that the action is not needed.
2242
        if ($canEdit && $isOnDraft) {
2243
            $majorActions->push(
2244
                FormAction::create('save', _t(__CLASS__.'.BUTTONSAVED', 'Saved'))
2245
                    ->addExtraClass('btn-secondary-outline font-icon-check-mark')
2246
                    ->setAttribute('data-btn-alternate', 'btn action btn-primary font-icon-save')
2247
                    ->setUseButtonTag(true)
2248
                    ->setAttribute('data-text-alternate', _t('SilverStripe\\CMS\\Controllers\\CMSMain.SAVEDRAFT', 'Save draft'))
2249
            );
2250
        }
2251
2252
        if ($canPublish && $isOnDraft) {
2253
            // "publish", as with "save", it supports an alternate state to show when action is needed.
2254
            $majorActions->push(
2255
                $publish = FormAction::create('publish', _t(__CLASS__.'.BUTTONPUBLISHED', 'Published'))
2256
                    ->addExtraClass('btn-secondary-outline font-icon-check-mark')
2257
                    ->setAttribute('data-btn-alternate', 'btn action btn-primary font-icon-rocket')
2258
                    ->setUseButtonTag(true)
2259
                    ->setAttribute('data-text-alternate', _t(__CLASS__.'.BUTTONSAVEPUBLISH', 'Save & publish'))
2260
            );
2261
2262
            // Set up the initial state of the button to reflect the state of the underlying SiteTree object.
2263
            if ($stagesDiffer) {
2264
                $publish->addExtraClass('btn-primary font-icon-rocket');
2265
                $publish->setTitle(_t(__CLASS__.'.BUTTONSAVEPUBLISH', 'Save & publish'));
2266
                $publish->removeExtraClass('btn-secondary-outline font-icon-check-mark');
2267
            }
2268
        }
2269
2270
        $actions = new FieldList(array($majorActions, $rootTabSet));
2271
2272
        // Hook for extensions to add/remove actions.
2273
        $this->extend('updateCMSActions', $actions);
2274
2275
        return $actions;
2276
    }
2277
2278
    public function onAfterPublish()
2279
    {
2280
        // Force live sort order to match stage sort order
2281
        DB::prepared_query(
2282
            'UPDATE "SiteTree_Live"
2283
			SET "Sort" = (SELECT "SiteTree"."Sort" FROM "SiteTree" WHERE "SiteTree_Live"."ID" = "SiteTree"."ID")
2284
			WHERE EXISTS (SELECT "SiteTree"."Sort" FROM "SiteTree" WHERE "SiteTree_Live"."ID" = "SiteTree"."ID") AND "ParentID" = ?',
2285
            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...
2286
        );
2287
    }
2288
2289
    /**
2290
     * Update draft dependant pages
2291
     */
2292
    public function onAfterRevertToLive()
2293
    {
2294
        // Use an alias to get the updates made by $this->publish
2295
        /** @var SiteTree $stageSelf */
2296
        $stageSelf = Versioned::get_by_stage(self::class, Versioned::DRAFT)->byID($this->ID);
2297
        $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...
2298
2299
        // Need to update pages linking to this one as no longer broken
2300
        foreach ($stageSelf->DependentPages() as $page) {
2301
            /** @var SiteTree $page */
2302
            $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...
2303
        }
2304
    }
2305
2306
    /**
2307
     * Determine if this page references a parent which is archived, and not available in stage
2308
     *
2309
     * @return bool True if there is an archived parent
2310
     */
2311
    protected function isParentArchived()
2312
    {
2313
        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...
2314
            /** @var SiteTree $parentPage */
2315
            $parentPage = Versioned::get_latest_version(self::class, $parentID);
2316
            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...
2317
                return true;
2318
            }
2319
        }
2320
        return false;
2321
    }
2322
2323
    /**
2324
     * Restore the content in the active copy of this SiteTree page to the stage site.
2325
     *
2326
     * @return self
2327
     */
2328
    public function doRestoreToStage()
2329
    {
2330
        $this->invokeWithExtensions('onBeforeRestoreToStage', $this);
2331
2332
        // Ensure that the parent page is restored, otherwise restore to root
2333
        if ($this->isParentArchived()) {
2334
            $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...
2335
        }
2336
2337
        // if no record can be found on draft stage (meaning it has been "deleted from draft" before),
2338
        // create an empty record
2339
        if (!DB::prepared_query("SELECT \"ID\" FROM \"SiteTree\" WHERE \"ID\" = ?", array($this->ID))->value()) {
2340
            $conn = DB::get_conn();
2341
            if (method_exists($conn, 'allowPrimaryKeyEditing')) {
2342
                $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...
2343
            }
2344
            DB::prepared_query("INSERT INTO \"SiteTree\" (\"ID\") VALUES (?)", array($this->ID));
2345
            if (method_exists($conn, 'allowPrimaryKeyEditing')) {
2346
                $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...
2347
            }
2348
        }
2349
2350
        $oldReadingMode = Versioned::get_reading_mode();
2351
        Versioned::set_stage(Versioned::DRAFT);
2352
        $this->forceChange();
2353
        $this->write();
2354
2355
        /** @var SiteTree $result */
2356
        $result = DataObject::get_by_id(self::class, $this->ID);
2357
2358
        Versioned::set_reading_mode($oldReadingMode);
2359
2360
        // Need to update pages linking to this one as no longer broken
2361
        $this->updateDependentPages();
2362
2363
        $this->invokeWithExtensions('onAfterRestoreToStage', $this);
2364
2365
        return $result;
2366
    }
2367
2368
    /**
2369
     * Check if this page is new - that is, if it has yet to have been written to the database.
2370
     *
2371
     * @return bool
2372
     */
2373
    public function isNew()
2374
    {
2375
        /**
2376
         * This check was a problem for a self-hosted site, and may indicate a bug in the interpreter on their server,
2377
         * or a bug here. Changing the condition from empty($this->ID) to !$this->ID && !$this->record['ID'] fixed this.
2378
         */
2379
        if (empty($this->ID)) {
2380
            return true;
2381
        }
2382
2383
        if (is_numeric($this->ID)) {
2384
            return false;
2385
        }
2386
2387
        return stripos($this->ID, 'new') === 0;
2388
    }
2389
2390
    /**
2391
     * Get the class dropdown used in the CMS to change the class of a page. This returns the list of options in the
2392
     * dropdown as a Map from class name to singular name. Filters by {@link SiteTree->canCreate()}, as well as
2393
     * {@link SiteTree::$needs_permission}.
2394
     *
2395
     * @return array
2396
     */
2397
    protected function getClassDropdown()
2398
    {
2399
        $classes = self::page_type_classes();
2400
        $currentClass = null;
2401
2402
        $result = array();
2403
        foreach ($classes as $class) {
2404
            $instance = singleton($class);
2405
2406
            // if the current page type is this the same as the class type always show the page type in the list
2407
            if ($this->ClassName != $instance->ClassName) {
2408
                if ($instance instanceof HiddenClass) {
2409
                    continue;
2410
                }
2411
                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...
2412
                    continue;
2413
                }
2414
            }
2415
2416
            if ($perms = $instance->stat('need_permission')) {
2417
                if (!$this->can($perms)) {
2418
                    continue;
2419
                }
2420
            }
2421
2422
            $pageTypeName = $instance->i18n_singular_name();
2423
2424
            $currentClass = $class;
2425
            $result[$class] = $pageTypeName;
2426
2427
            // If we're in translation mode, the link between the translated pagetype title and the actual classname
2428
            // might not be obvious, so we add it in parantheses. Example: class "RedirectorPage" has the title
2429
            // "Weiterleitung" in German, so it shows up as "Weiterleitung (RedirectorPage)"
2430
            if (i18n::getData()->langFromLocale(i18n::get_locale()) != 'en') {
2431
                $result[$class] = $result[$class] .  " ({$class})";
2432
            }
2433
        }
2434
2435
        // sort alphabetically, and put current on top
2436
        asort($result);
2437
        if ($currentClass) {
2438
            $currentPageTypeName = $result[$currentClass];
2439
            unset($result[$currentClass]);
2440
            $result = array_reverse($result);
2441
            $result[$currentClass] = $currentPageTypeName;
2442
            $result = array_reverse($result);
2443
        }
2444
2445
        return $result;
2446
    }
2447
2448
    /**
2449
     * Returns an array of the class names of classes that are allowed to be children of this class.
2450
     *
2451
     * @return string[]
2452
     */
2453
    public function allowedChildren()
2454
    {
2455
        // Get config based on old FIRST_SET rules
2456
        $candidates = null;
2457
        $class = get_class($this);
2458
        while ($class) {
2459
            if (Config::inst()->exists($class, 'allowed_children', Config::UNINHERITED)) {
2460
                $candidates = Config::inst()->get($class, 'allowed_children', Config::UNINHERITED);
2461
                break;
2462
            }
2463
            $class = get_parent_class($class);
2464
        }
2465
        if (!$candidates || $candidates === 'none' || $candidates === 'SiteTree_root') {
2466
            return [];
2467
        }
2468
2469
        // Parse candidate list
2470
        $allowedChildren = [];
2471
        foreach ($candidates as $candidate) {
2472
            // If a classname is prefixed by "*", such as "*Page", then only that class is allowed - no subclasses.
2473
            // Otherwise, the class and all its subclasses are allowed.
2474
            if (substr($candidate, 0, 1) == '*') {
2475
                $allowedChildren[] = substr($candidate, 1);
2476
            } elseif ($subclasses = ClassInfo::subclassesFor($candidate)) {
2477
                foreach ($subclasses as $subclass) {
2478
                    if ($subclass == 'SiteTree_root' || singleton($subclass) instanceof HiddenClass) {
2479
                        continue;
2480
                    }
2481
                    $allowedChildren[] = $subclass;
2482
                }
2483
            }
2484
        }
2485
        return $allowedChildren;
2486
    }
2487
2488
    /**
2489
     * Returns the class name of the default class for children of this page.
2490
     *
2491
     * @return string
2492
     */
2493
    public function defaultChild()
2494
    {
2495
        $default = $this->stat('default_child');
2496
        $allowed = $this->allowedChildren();
2497
        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...
2498
            if (!$default || !in_array($default, $allowed)) {
2499
                $default = reset($allowed);
2500
            }
2501
            return $default;
2502
        }
2503
        return null;
2504
    }
2505
2506
    /**
2507
     * Returns the class name of the default class for the parent of this page.
2508
     *
2509
     * @return string
2510
     */
2511
    public function defaultParent()
2512
    {
2513
        return $this->stat('default_parent');
2514
    }
2515
2516
    /**
2517
     * Get the title for use in menus for this page. If the MenuTitle field is set it returns that, else it returns the
2518
     * Title field.
2519
     *
2520
     * @return string
2521
     */
2522
    public function getMenuTitle()
2523
    {
2524
        if ($value = $this->getField("MenuTitle")) {
2525
            return $value;
2526
        } else {
2527
            return $this->getField("Title");
2528
        }
2529
    }
2530
2531
2532
    /**
2533
     * Set the menu title for this page.
2534
     *
2535
     * @param string $value
2536
     */
2537
    public function setMenuTitle($value)
2538
    {
2539
        if ($value == $this->getField("Title")) {
2540
            $this->setField("MenuTitle", null);
2541
        } else {
2542
            $this->setField("MenuTitle", $value);
2543
        }
2544
    }
2545
2546
    /**
2547
     * A flag provides the user with additional data about the current page status, for example a "removed from draft"
2548
     * status. Each page can have more than one status flag. Returns a map of a unique key to a (localized) title for
2549
     * the flag. The unique key can be reused as a CSS class. Use the 'updateStatusFlags' extension point to customize
2550
     * the flags.
2551
     *
2552
     * Example (simple):
2553
     *   "deletedonlive" => "Deleted"
2554
     *
2555
     * Example (with optional title attribute):
2556
     *   "deletedonlive" => array('text' => "Deleted", 'title' => 'This page has been deleted')
2557
     *
2558
     * @param bool $cached Whether to serve the fields from cache; false regenerate them
2559
     * @return array
2560
     */
2561
    public function getStatusFlags($cached = true)
2562
    {
2563
        if (!$this->_cache_statusFlags || !$cached) {
2564
            $flags = array();
2565
            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...
2566
                $flags['removedfromdraft'] = array(
2567
                    'text' => _t(__CLASS__.'.ONLIVEONLYSHORT', 'On live only'),
2568
                    'title' => _t(__CLASS__.'.ONLIVEONLYSHORTHELP', 'Page is published, but has been deleted from draft'),
2569
                );
2570
            } 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...
2571
                $flags['archived'] = array(
2572
                    'text' => _t(__CLASS__.'.ARCHIVEDPAGESHORT', 'Archived'),
2573
                    'title' => _t(__CLASS__.'.ARCHIVEDPAGEHELP', 'Page is removed from draft and live'),
2574
                );
2575
            } 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...
2576
                $flags['addedtodraft'] = array(
2577
                    'text' => _t(__CLASS__.'.ADDEDTODRAFTSHORT', 'Draft'),
2578
                    'title' => _t(__CLASS__.'.ADDEDTODRAFTHELP', "Page has not been published yet")
2579
                );
2580
            } 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...
2581
                $flags['modified'] = array(
2582
                    'text' => _t(__CLASS__.'.MODIFIEDONDRAFTSHORT', 'Modified'),
2583
                    'title' => _t(__CLASS__.'.MODIFIEDONDRAFTHELP', 'Page has unpublished changes'),
2584
                );
2585
            }
2586
2587
            $this->extend('updateStatusFlags', $flags);
2588
2589
            $this->_cache_statusFlags = $flags;
2590
        }
2591
2592
        return $this->_cache_statusFlags;
2593
    }
2594
2595
    /**
2596
     * getTreeTitle will return three <span> html DOM elements, an empty <span> with the class 'jstree-pageicon' in
2597
     * front, following by a <span> wrapping around its MenutTitle, then following by a <span> indicating its
2598
     * publication status.
2599
     *
2600
     * @return string An HTML string ready to be directly used in a template
2601
     */
2602
    public function getTreeTitle()
2603
    {
2604
        // Build the list of candidate children
2605
        $children = array();
2606
        $candidates = static::page_type_classes();
2607
        foreach ($this->allowedChildren() as $childClass) {
2608
            if (!in_array($childClass, $candidates)) {
2609
                continue;
2610
            }
2611
            $child = singleton($childClass);
2612
            if ($child->canCreate(null, array('Parent' => $this))) {
2613
                $children[$childClass] = $child->i18n_singular_name();
2614
            }
2615
        }
2616
        $flags = $this->getStatusFlags();
2617
        $treeTitle = sprintf(
2618
            "<span class=\"jstree-pageicon\"></span><span class=\"item\" data-allowedchildren=\"%s\">%s</span>",
2619
            Convert::raw2att(Convert::raw2json($children)),
2620
            Convert::raw2xml(str_replace(array("\n","\r"), "", $this->MenuTitle))
2621
        );
2622
        foreach ($flags as $class => $data) {
2623
            if (is_string($data)) {
2624
                $data = array('text' => $data);
2625
            }
2626
            $treeTitle .= sprintf(
2627
                "<span class=\"badge %s\"%s>%s</span>",
2628
                'status-' . Convert::raw2xml($class),
2629
                (isset($data['title'])) ? sprintf(' title="%s"', Convert::raw2xml($data['title'])) : '',
2630
                Convert::raw2xml($data['text'])
2631
            );
2632
        }
2633
2634
        return $treeTitle;
2635
    }
2636
2637
    /**
2638
     * Returns the page in the current page stack of the given level. Level(1) will return the main menu item that
2639
     * we're currently inside, etc.
2640
     *
2641
     * @param int $level
2642
     * @return SiteTree
2643
     */
2644
    public function Level($level)
2645
    {
2646
        $parent = $this;
2647
        $stack = array($parent);
2648
        while (($parent = $parent->Parent()) && $parent->exists()) {
2649
            array_unshift($stack, $parent);
2650
        }
2651
2652
        return isset($stack[$level-1]) ? $stack[$level-1] : null;
2653
    }
2654
2655
    /**
2656
     * Gets the depth of this page in the sitetree, where 1 is the root level
2657
     *
2658
     * @return int
2659
     */
2660
    public function getPageLevel()
2661
    {
2662
        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...
2663
            return 1 + $this->Parent()->getPageLevel();
2664
        }
2665
        return 1;
2666
    }
2667
2668
    /**
2669
     * Find the controller name by our convention of {$ModelClass}Controller
2670
     *
2671
     * @return string
2672
     */
2673
    public function getControllerName()
2674
    {
2675
        //default controller for SiteTree objects
2676
        $controller = ContentController::class;
2677
2678
        //go through the ancestry for this class looking for
2679
        $ancestry = ClassInfo::ancestry(static::class);
2680
        // loop over the array going from the deepest descendant (ie: the current class) to SiteTree
2681
        while ($class = array_pop($ancestry)) {
2682
            //we don't need to go any deeper than the SiteTree class
2683
            if ($class == SiteTree::class) {
2684
                break;
2685
            }
2686
            // If we have a class of "{$ClassName}Controller" then we found our controller
2687
            if (class_exists($candidate = sprintf('%sController', $class))) {
2688
                $controller = $candidate;
2689
                break;
2690
            } elseif (class_exists($candidate = sprintf('%s_Controller', $class))) {
2691
                // Support the legacy underscored filename, but raise a deprecation notice
2692
                Deprecation::notice(
2693
                    '5.0',
2694
                    'Underscored controller class names are deprecated. Use "MyController" instead of "My_Controller".',
2695
                    Deprecation::SCOPE_GLOBAL
2696
                );
2697
                $controller = $candidate;
2698
                break;
2699
            }
2700
        }
2701
2702
        return $controller;
2703
    }
2704
2705
    /**
2706
     * Return the CSS classes to apply to this node in the CMS tree.
2707
     *
2708
     * @return string
2709
     */
2710
    public function CMSTreeClasses()
2711
    {
2712
        $classes = sprintf('class-%s', static::class);
2713
        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...
2714
            $classes .= " BrokenLink";
2715
        }
2716
2717
        if (!$this->canAddChildren()) {
2718
            $classes .= " nochildren";
2719
        }
2720
2721
        if (!$this->canEdit() && !$this->canAddChildren()) {
2722
            if (!$this->canView()) {
2723
                $classes .= " disabled";
2724
            } else {
2725
                $classes .= " edit-disabled";
2726
            }
2727
        }
2728
2729
        if (!$this->ShowInMenus) {
2730
            $classes .= " notinmenu";
2731
        }
2732
2733
        return $classes;
2734
    }
2735
2736
    /**
2737
     * Stops extendCMSFields() being called on getCMSFields(). This is useful when you need access to fields added by
2738
     * subclasses of SiteTree in a extension. Call before calling parent::getCMSFields(), and reenable afterwards.
2739
     */
2740
    public static function disableCMSFieldsExtensions()
2741
    {
2742
        self::$runCMSFieldsExtensions = false;
2743
    }
2744
2745
    /**
2746
     * Reenables extendCMSFields() being called on getCMSFields() after it has been disabled by
2747
     * disableCMSFieldsExtensions().
2748
     */
2749
    public static function enableCMSFieldsExtensions()
2750
    {
2751
        self::$runCMSFieldsExtensions = true;
2752
    }
2753
2754
    public function providePermissions()
2755
    {
2756
        return array(
2757
            'SITETREE_GRANT_ACCESS' => array(
2758
                'name' => _t(__CLASS__.'.PERMISSION_GRANTACCESS_DESCRIPTION', 'Manage access rights for content'),
2759
                'help' => _t(__CLASS__.'.PERMISSION_GRANTACCESS_HELP', 'Allow setting of page-specific access restrictions in the "Pages" section.'),
2760
                'category' => _t('SilverStripe\\Security\\Permission.PERMISSIONS_CATEGORY', 'Roles and access permissions'),
2761
                'sort' => 100
2762
            ),
2763
            'SITETREE_VIEW_ALL' => array(
2764
                'name' => _t(__CLASS__.'.VIEW_ALL_DESCRIPTION', 'View any page'),
2765
                'category' => _t('SilverStripe\\Security\\Permission.CONTENT_CATEGORY', 'Content permissions'),
2766
                'sort' => -100,
2767
                'help' => _t(__CLASS__.'.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')
2768
            ),
2769
            'SITETREE_EDIT_ALL' => array(
2770
                'name' => _t(__CLASS__.'.EDIT_ALL_DESCRIPTION', 'Edit any page'),
2771
                'category' => _t('SilverStripe\\Security\\Permission.CONTENT_CATEGORY', 'Content permissions'),
2772
                'sort' => -50,
2773
                'help' => _t(__CLASS__.'.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')
2774
            ),
2775
            'SITETREE_REORGANISE' => array(
2776
                'name' => _t(__CLASS__.'.REORGANISE_DESCRIPTION', 'Change site structure'),
2777
                'category' => _t('SilverStripe\\Security\\Permission.CONTENT_CATEGORY', 'Content permissions'),
2778
                'help' => _t(__CLASS__.'.REORGANISE_HELP', 'Rearrange pages in the site tree through drag&drop.'),
2779
                'sort' => 100
2780
            ),
2781
            'VIEW_DRAFT_CONTENT' => array(
2782
                'name' => _t(__CLASS__.'.VIEW_DRAFT_CONTENT', 'View draft content'),
2783
                'category' => _t('SilverStripe\\Security\\Permission.CONTENT_CATEGORY', 'Content permissions'),
2784
                'help' => _t(__CLASS__.'.VIEW_DRAFT_CONTENT_HELP', 'Applies to viewing pages outside of the CMS in draft mode. Useful for external collaborators without CMS access.'),
2785
                'sort' => 100
2786
            )
2787
        );
2788
    }
2789
2790
    /**
2791
     * Default singular name for page / sitetree
2792
     *
2793
     * @return string
2794
     */
2795 View Code Duplication
    public function singular_name()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in 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...
2796
    {
2797
        $base = in_array(static::class, [Page::class, self::class]);
2798
        if ($base) {
2799
            return $this->stat('base_singular_name');
2800
        }
2801
        return parent::singular_name();
2802
    }
2803
2804
    /**
2805
     * Default plural name for page / sitetree
2806
     *
2807
     * @return string
2808
     */
2809 View Code Duplication
    public function plural_name()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in 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...
2810
    {
2811
        $base = in_array(static::class, [Page::class, self::class]);
2812
        if ($base) {
2813
            return $this->stat('base_plural_name');
2814
        }
2815
        return parent::plural_name();
2816
    }
2817
2818
    /**
2819
     * Get description for this page type
2820
     *
2821
     * @return string|null
2822
     */
2823
    public function classDescription()
2824
    {
2825
        $base = in_array(static::class, [Page::class, self::class]);
2826
        if ($base) {
2827
            return $this->stat('base_description');
2828
        }
2829
        return $this->stat('description');
2830
    }
2831
2832
    /**
2833
     * Get localised description for this page
2834
     *
2835
     * @return string|null
2836
     */
2837
    public function i18n_classDescription()
2838
    {
2839
        $description = $this->classDescription();
2840
        if ($description) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $description 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...
2841
            return _t(static::class.'.DESCRIPTION', $description);
2842
        }
2843
        return null;
2844
    }
2845
2846
    /**
2847
     * Overloaded to also provide entities for 'Page' class which is usually located in custom code, hence textcollector
2848
     * picks it up for the wrong folder.
2849
     *
2850
     * @return array
2851
     */
2852
    public function provideI18nEntities()
2853
    {
2854
        $entities = parent::provideI18nEntities();
2855
2856
        // Add optional description
2857
        $description = $this->classDescription();
2858
        if ($description) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $description 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...
2859
            $entities[static::class . '.DESCRIPTION'] = $description;
2860
        }
2861
        return $entities;
2862
    }
2863
2864
    /**
2865
     * Returns 'root' if the current page has no parent, or 'subpage' otherwise
2866
     *
2867
     * @return string
2868
     */
2869
    public function getParentType()
2870
    {
2871
        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...
2872
    }
2873
2874
    /**
2875
     * Clear the permissions cache for SiteTree
2876
     */
2877
    public static function reset()
2878
    {
2879
        $permissions = static::getPermissionChecker();
2880
        if ($permissions instanceof InheritedPermissions) {
2881
            $permissions->clearCache();
2882
        }
2883
    }
2884
2885
    /**
2886
     * Update dependant pages
2887
     */
2888
    protected function updateDependentPages()
2889
    {
2890
        // Need to flush cache to avoid outdated versionnumber references
2891
        $this->flushCache();
2892
2893
        // Need to mark pages depending to this one as broken
2894
        $dependentPages = $this->DependentPages();
2895
        if ($dependentPages) {
2896
            foreach ($dependentPages as $page) {
2897
                // $page->write() calls syncLinkTracking, which does all the hard work for us.
2898
                $page->write();
2899
            }
2900
        }
2901
    }
2902
}
2903