Completed
Push — master ( 0fbdad...a03f6b )
by Daniel
16:02
created

SiteTree::canCreate()   C

Complexity

Conditions 11
Paths 40

Size

Total Lines 34
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 34
rs 5.2653
c 0
b 0
f 0
cc 11
eloc 16
nc 40
nop 2

How to fix   Complexity   

Long Method

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

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

Commonly applied refactorings include:

1
<?php
2
3
namespace SilverStripe\CMS\Model;
4
5
use Page;
6
use SilverStripe\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" => VirtualPage::class . '.CopyContentFrom'
197
    );
198
199
    private static $owned_by = array(
200
        "VirtualPages"
201
    );
202
203
    private static $cascade_deletes = [
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
        'VirtualPages',
205
    ];
206
207
    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...
208
        "Breadcrumbs" => "HTMLFragment",
209
        "LastEdited" => "Datetime",
210
        "Created" => "Datetime",
211
        'Link' => 'Text',
212
        'RelativeLink' => 'Text',
213
        'AbsoluteLink' => 'Text',
214
        'CMSEditLink' => 'Text',
215
        'TreeTitle' => 'HTMLFragment',
216
        'MetaTags' => 'HTMLFragment',
217
    );
218
219
    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...
220
        "ShowInMenus" => 1,
221
        "ShowInSearch" => 1,
222
    );
223
224
    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...
225
226
    private static $versioning = array(
227
        "Stage",  "Live"
228
    );
229
230
    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...
231
232
    /**
233
     * If this is false, the class cannot be created in the CMS by regular content authors, only by ADMINs.
234
     * @var boolean
235
     * @config
236
     */
237
    private static $can_create = true;
238
239
    /**
240
     * Icon to use in the CMS page tree. This should be the full filename, relative to the webroot.
241
     * Also supports custom CSS rule contents (applied to the correct selector for the tree UI implementation).
242
     *
243
     * @see CMSMain::generateTreeStylingCSS()
244
     * @config
245
     * @var string
246
     */
247
    private static $icon = null;
248
249
    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...
250
        Hierarchy::class,
251
        Versioned::class,
252
        SiteTreeLinkTracking::class,
253
        InheritedPermissionsExtension::class,
254
    ];
255
256
    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...
257
        'Title',
258
        'Content',
259
    );
260
261
    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...
262
        'URLSegment' => 'URL'
263
    );
264
265
    /**
266
     * @config
267
     */
268
    private static $nested_urls = true;
269
270
    /**
271
     * @config
272
    */
273
    private static $create_default_pages = true;
274
275
    /**
276
     * This controls whether of not extendCMSFields() is called by getCMSFields.
277
     */
278
    private static $runCMSFieldsExtensions = true;
279
280
    /**
281
     * @config
282
     * @var boolean
283
     */
284
    private static $enforce_strict_hierarchy = true;
285
286
    /**
287
     * The value used for the meta generator tag. Leave blank to omit the tag.
288
     *
289
     * @config
290
     * @var string
291
     */
292
    private static $meta_generator = 'SilverStripe - http://silverstripe.org';
293
294
    protected $_cache_statusFlags = null;
295
296
    /**
297
     * Plural form for SiteTree / Page classes. Not inherited by subclasses.
298
     *
299
     * @config
300
     * @var string
301
     */
302
    private static $base_plural_name = 'Pages';
303
304
    /**
305
     * Plural form for SiteTree / Page classes. Not inherited by subclasses.
306
     *
307
     * @config
308
     * @var string
309
     */
310
    private static $base_singular_name = 'Page';
311
312
    /**
313
     * Description of the class functionality, typically shown to a user
314
     * when selecting which page type to create. Translated through {@link provideI18nEntities()}.
315
     *
316
     * @see SiteTree::classDescription()
317
     * @see SiteTree::i18n_classDescription()
318
     *
319
     * @config
320
     * @var string
321
     */
322
    private static $description = null;
323
324
    /**
325
     * Description for Page and SiteTree classes, but not inherited by subclasses.
326
     * override SiteTree::$description in subclasses instead.
327
     *
328
     * @see SiteTree::classDescription()
329
     * @see SiteTree::i18n_classDescription()
330
     *
331
     * @config
332
     * @var string
333
     */
334
    private static $base_description = 'Generic content page';
335
336
    /**
337
     * Fetches the {@link SiteTree} object that maps to a link.
338
     *
339
     * If you have enabled {@link SiteTree::config()->nested_urls} on this site, then you can use a nested link such as
340
     * "about-us/staff/", and this function will traverse down the URL chain and grab the appropriate link.
341
     *
342
     * Note that if no model can be found, this method will fall over to a extended alternateGetByLink method provided
343
     * by a extension attached to {@link SiteTree}
344
     *
345
     * @param string $link  The link of the page to search for
346
     * @param bool   $cache True (default) to use caching, false to force a fresh search from the database
347
     * @return SiteTree
348
     */
349
    public static function get_by_link($link, $cache = true)
350
    {
351
        if (trim($link, '/')) {
352
            $link = trim(Director::makeRelative($link), '/');
353
        } else {
354
            $link = RootURLController::get_homepage_link();
355
        }
356
357
        $parts = preg_split('|/+|', $link);
358
359
        // Grab the initial root level page to traverse down from.
360
        $URLSegment = array_shift($parts);
361
        $conditions = array('"SiteTree"."URLSegment"' => rawurlencode($URLSegment));
362
        if (self::config()->nested_urls) {
363
            $conditions[] = array('"SiteTree"."ParentID"' => 0);
364
        }
365
        /** @var SiteTree $sitetree */
366
        $sitetree = DataObject::get_one(self::class, $conditions, $cache);
367
368
        /// Fall back on a unique URLSegment for b/c.
369
        if (!$sitetree
370
            && self::config()->nested_urls
371
            && $sitetree = DataObject::get_one(self::class, array(
372
                '"SiteTree"."URLSegment"' => $URLSegment
373
            ), $cache)
374
        ) {
375
            return $sitetree;
376
        }
377
378
        // Attempt to grab an alternative page from extensions.
379
        if (!$sitetree) {
380
            $parentID = self::config()->nested_urls ? 0 : null;
381
382 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...
383
                foreach ($alternatives as $alternative) {
384
                    if ($alternative) {
385
                        $sitetree = $alternative;
386
                    }
387
                }
388
            }
389
390
            if (!$sitetree) {
391
                return null;
392
            }
393
        }
394
395
        // Check if we have any more URL parts to parse.
396
        if (!self::config()->nested_urls || !count($parts)) {
397
            return $sitetree;
398
        }
399
400
        // Traverse down the remaining URL segments and grab the relevant SiteTree objects.
401
        foreach ($parts as $segment) {
402
            $next = DataObject::get_one(
403
                self::class,
404
                array(
405
                    '"SiteTree"."URLSegment"' => $segment,
406
                    '"SiteTree"."ParentID"' => $sitetree->ID
407
                ),
408
                $cache
409
            );
410
411
            if (!$next) {
412
                $parentID = (int) $sitetree->ID;
413
414 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...
415
                    foreach ($alternatives as $alternative) {
416
                        if ($alternative) {
417
                            $next = $alternative;
418
                        }
419
                    }
420
                }
421
422
                if (!$next) {
423
                    return null;
424
                }
425
            }
426
427
            $sitetree->destroy();
428
            $sitetree = $next;
429
        }
430
431
        return $sitetree;
432
    }
433
434
    /**
435
     * Return a subclass map of SiteTree that shouldn't be hidden through {@link SiteTree::$hide_ancestor}
436
     *
437
     * @return array
438
     */
439
    public static function page_type_classes()
440
    {
441
        $classes = ClassInfo::getValidSubClasses();
442
443
        $baseClassIndex = array_search(self::class, $classes);
444
        if ($baseClassIndex !== false) {
445
            unset($classes[$baseClassIndex]);
446
        }
447
448
        $kill_ancestors = array();
449
450
        // figure out if there are any classes we don't want to appear
451
        foreach ($classes as $class) {
452
            $instance = singleton($class);
453
454
            // do any of the progeny want to hide an ancestor?
455
            if ($ancestor_to_hide = $instance->stat('hide_ancestor')) {
456
                // note for killing later
457
                $kill_ancestors[] = $ancestor_to_hide;
458
            }
459
        }
460
461
        // If any of the descendents don't want any of the elders to show up, cruelly render the elders surplus to
462
        // requirements
463
        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...
464
            $kill_ancestors = array_unique($kill_ancestors);
465
            foreach ($kill_ancestors as $mark) {
466
                // unset from $classes
467
                $idx = array_search($mark, $classes, true);
468
                if ($idx !== false) {
469
                    unset($classes[$idx]);
470
                }
471
            }
472
        }
473
474
        return $classes;
475
    }
476
477
    /**
478
     * Replace a "[sitetree_link id=n]" shortcode with a link to the page with the corresponding ID.
479
     *
480
     * @param array      $arguments
481
     * @param string     $content
482
     * @param ShortcodeParser $parser
483
     * @return string
484
     */
485
    public static function link_shortcode_handler($arguments, $content = null, $parser = null)
486
    {
487
        if (!isset($arguments['id']) || !is_numeric($arguments['id'])) {
488
            return null;
489
        }
490
491
        /** @var SiteTree $page */
492
        if (!($page = DataObject::get_by_id(self::class, $arguments['id']))         // Get the current page by ID.
493
            && !($page = Versioned::get_latest_version(self::class, $arguments['id'])) // Attempt link to old version.
494
        ) {
495
             return null; // There were no suitable matches at all.
496
        }
497
498
        /** @var SiteTree $page */
499
        $link = Convert::raw2att($page->Link());
500
501
        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...
502
            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...
503
        } else {
504
            return $link;
505
        }
506
    }
507
508
    /**
509
     * Return the link for this {@link SiteTree} object, with the {@link Director::baseURL()} included.
510
     *
511
     * @param string $action Optional controller action (method).
512
     *                       Note: URI encoding of this parameter is applied automatically through template casting,
513
     *                       don't encode the passed parameter. Please use {@link Controller::join_links()} instead to
514
     *                       append GET parameters.
515
     * @return string
516
     */
517
    public function Link($action = null)
518
    {
519
        return Controller::join_links(Director::baseURL(), $this->RelativeLink($action));
520
    }
521
522
    /**
523
     * Get the absolute URL for this page, including protocol and host.
524
     *
525
     * @param string $action See {@link Link()}
526
     * @return string
527
     */
528
    public function AbsoluteLink($action = null)
529
    {
530
        if ($this->hasMethod('alternateAbsoluteLink')) {
531
            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...
532
        } else {
533
            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 533 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...
534
        }
535
    }
536
537
    /**
538
     * Base link used for previewing. Defaults to absolute URL, in order to account for domain changes, e.g. on multi
539
     * site setups. Does not contain hints about the stage, see {@link SilverStripeNavigator} for details.
540
     *
541
     * @param string $action See {@link Link()}
542
     * @return string
543
     */
544
    public function PreviewLink($action = null)
545
    {
546
        if ($this->hasMethod('alternatePreviewLink')) {
547
            Deprecation::notice('5.0', 'Use updatePreviewLink or override PreviewLink method');
548
            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...
549
        }
550
551
        $link = $this->AbsoluteLink($action);
552
        $this->extend('updatePreviewLink', $link, $action);
553
        return $link;
554
    }
555
556
    public function getMimeType()
557
    {
558
        return 'text/html';
559
    }
560
561
    /**
562
     * Return the link for this {@link SiteTree} object relative to the SilverStripe root.
563
     *
564
     * By default, if this page is the current home page, and there is no action specified then this will return a link
565
     * to the root of the site. However, if you set the $action parameter to TRUE then the link will not be rewritten
566
     * and returned in its full form.
567
     *
568
     * @uses RootURLController::get_homepage_link()
569
     *
570
     * @param string $action See {@link Link()}
571
     * @return string
572
     */
573
    public function RelativeLink($action = null)
574
    {
575
        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...
576
            $parent = $this->Parent();
577
            // If page is removed select parent from version history (for archive page view)
578
            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...
579
                $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...
580
            }
581
            $base = $parent->RelativeLink($this->URLSegment);
582
        } 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...
583
            // Unset base for root-level homepages.
584
            // Note: Homepages with action parameters (or $action === true)
585
            // need to retain their URLSegment.
586
            $base = null;
587
        } else {
588
            $base = $this->URLSegment;
589
        }
590
591
        $this->extend('updateRelativeLink', $base, $action);
592
593
        // Legacy support: If $action === true, retain URLSegment for homepages,
594
        // but don't append any action
595
        if ($action === true) {
596
            $action = null;
597
        }
598
599
        return Controller::join_links($base, '/', $action);
600
    }
601
602
    /**
603
     * Get the absolute URL for this page on the Live site.
604
     *
605
     * @param bool $includeStageEqualsLive Whether to append the URL with ?stage=Live to force Live mode
606
     * @return string
607
     */
608
    public function getAbsoluteLiveLink($includeStageEqualsLive = true)
609
    {
610
        $oldReadingMode = Versioned::get_reading_mode();
611
        Versioned::set_stage(Versioned::LIVE);
612
        /** @var SiteTree $live */
613
        $live = Versioned::get_one_by_stage(self::class, Versioned::LIVE, array(
614
            '"SiteTree"."ID"' => $this->ID
615
        ));
616
        if ($live) {
617
            $link = $live->AbsoluteLink();
618
            if ($includeStageEqualsLive) {
619
                $link = Controller::join_links($link, '?stage=Live');
620
            }
621
        } else {
622
            $link = null;
623
        }
624
625
        Versioned::set_reading_mode($oldReadingMode);
626
        return $link;
627
    }
628
629
    /**
630
     * Generates a link to edit this page in the CMS.
631
     *
632
     * @return string
633
     */
634
    public function CMSEditLink()
635
    {
636
        $link = Controller::join_links(
637
            CMSPageEditController::singleton()->Link('show'),
638
            $this->ID
639
        );
640
        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 640 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...
641
    }
642
643
644
    /**
645
     * Return a CSS identifier generated from this page's link.
646
     *
647
     * @return string The URL segment
648
     */
649
    public function ElementName()
650
    {
651
        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...
652
    }
653
654
    /**
655
     * Returns true if this is the currently active page being used to handle this request.
656
     *
657
     * @return bool
658
     */
659
    public function isCurrent()
660
    {
661
        $currentPage = Director::get_current_page();
662
        if ($currentPage instanceof ContentController) {
663
            $currentPage = $currentPage->data();
664
        }
665
        if ($currentPage instanceof SiteTree) {
666
            return $currentPage === $this || $currentPage->ID === $this->ID;
667
        }
668
        return false;
669
    }
670
671
    /**
672
     * Check if this page is in the currently active section (e.g. it is either current or one of its children is
673
     * currently being viewed).
674
     *
675
     * @return bool
676
     */
677
    public function isSection()
678
    {
679
        return $this->isCurrent() || (
680
            Director::get_current_page() instanceof SiteTree && in_array($this->ID, Director::get_current_page()->getAncestors()->column())
681
        );
682
    }
683
684
    /**
685
     * Check if the parent of this page has been removed (or made otherwise unavailable), and is still referenced by
686
     * this child. Any such orphaned page may still require access via the CMS, but should not be shown as accessible
687
     * to external users.
688
     *
689
     * @return bool
690
     */
691
    public function isOrphaned()
692
    {
693
        // Always false for root pages
694
        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...
695
            return false;
696
        }
697
698
        // Parent must exist and not be an orphan itself
699
        $parent = $this->Parent();
700
        return !$parent || !$parent->exists() || $parent->isOrphaned();
701
    }
702
703
    /**
704
     * Return "link" or "current" depending on if this is the {@link SiteTree::isCurrent()} current page.
705
     *
706
     * @return string
707
     */
708
    public function LinkOrCurrent()
709
    {
710
        return $this->isCurrent() ? 'current' : 'link';
711
    }
712
713
    /**
714
     * Return "link" or "section" depending on if this is the {@link SiteTree::isSeciton()} current section.
715
     *
716
     * @return string
717
     */
718
    public function LinkOrSection()
719
    {
720
        return $this->isSection() ? 'section' : 'link';
721
    }
722
723
    /**
724
     * Return "link", "current" or "section" depending on if this page is the current page, or not on the current page
725
     * but in the current section.
726
     *
727
     * @return string
728
     */
729
    public function LinkingMode()
730
    {
731
        if ($this->isCurrent()) {
732
            return 'current';
733
        } elseif ($this->isSection()) {
734
            return 'section';
735
        } else {
736
            return 'link';
737
        }
738
    }
739
740
    /**
741
     * Check if this page is in the given current section.
742
     *
743
     * @param string $sectionName Name of the section to check
744
     * @return bool True if we are in the given section
745
     */
746
    public function InSection($sectionName)
747
    {
748
        $page = Director::get_current_page();
749
        while ($page && $page->exists()) {
750
            if ($sectionName == $page->URLSegment) {
751
                return true;
752
            }
753
            $page = $page->Parent();
754
        }
755
        return false;
756
    }
757
758
    /**
759
     * Reset Sort on duped page
760
     *
761
     * @param SiteTree $original
762
     * @param bool $doWrite
763
     */
764
    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...
765
    {
766
        $this->Sort = 0;
767
    }
768
769
    /**
770
     * Duplicates each child of this node recursively and returns the top-level duplicate node.
771
     *
772
     * @return static The duplicated object
773
     */
774
    public function duplicateWithChildren()
775
    {
776
        /** @var SiteTree $clone */
777
        $clone = $this->duplicate();
778
        $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...
779
780
        if ($children) {
781
            /** @var SiteTree $child */
782
            $sort = 0;
783
            foreach ($children as $child) {
784
                $childClone = method_exists($child, 'duplicateWithChildren')
785
                    ? $child->duplicateWithChildren()
786
                    : $child->duplicate();
787
                $childClone->ParentID = $clone->ID;
788
                //retain sort order by manually setting sort values
789
                $childClone->Sort = ++$sort;
790
                $childClone->write();
791
            }
792
        }
793
794
        return $clone;
795
    }
796
797
    /**
798
     * Duplicate this node and its children as a child of the node with the given ID
799
     *
800
     * @param int $id ID of the new node's new parent
801
     */
802
    public function duplicateAsChild($id)
803
    {
804
        /** @var SiteTree $newSiteTree */
805
        $newSiteTree = $this->duplicate();
806
        $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...
807
        $newSiteTree->Sort = 0;
808
        $newSiteTree->write();
809
    }
810
811
    /**
812
     * Return a breadcrumb trail to this page. Excludes "hidden" pages (with ShowInMenus=0) by default.
813
     *
814
     * @param int $maxDepth The maximum depth to traverse.
815
     * @param boolean $unlinked Whether to link page titles.
816
     * @param boolean|string $stopAtPageType ClassName of a page to stop the upwards traversal.
817
     * @param boolean $showHidden Include pages marked with the attribute ShowInMenus = 0
818
     * @return string The breadcrumb trail.
819
     */
820
    public function Breadcrumbs($maxDepth = 20, $unlinked = false, $stopAtPageType = false, $showHidden = false, $delimiter = '&raquo;')
821
    {
822
        $pages = $this->getBreadcrumbItems($maxDepth, $stopAtPageType, $showHidden);
823
        $template = SSViewer::create('BreadcrumbsTemplate');
824
        return $template->process($this->customise(new ArrayData(array(
825
            "Pages" => $pages,
826
            "Unlinked" => $unlinked,
827
            "Delimiter" => $delimiter,
828
        ))));
829
    }
830
831
832
    /**
833
     * Returns a list of breadcrumbs for the current page.
834
     *
835
     * @param int $maxDepth The maximum depth to traverse.
836
     * @param boolean|string $stopAtPageType ClassName of a page to stop the upwards traversal.
837
     * @param boolean $showHidden Include pages marked with the attribute ShowInMenus = 0
838
     *
839
     * @return ArrayList
840
    */
841
    public function getBreadcrumbItems($maxDepth = 20, $stopAtPageType = false, $showHidden = false)
842
    {
843
        $page = $this;
844
        $pages = array();
845
846
        while ($page
847
            && $page->exists()
848
            && (!$maxDepth || count($pages) < $maxDepth)
849
            && (!$stopAtPageType || $page->ClassName != $stopAtPageType)
850
        ) {
851
            if ($showHidden || $page->ShowInMenus || ($page->ID == $this->ID)) {
852
                $pages[] = $page;
853
            }
854
855
            $page = $page->Parent();
856
        }
857
858
        return new ArrayList(array_reverse($pages));
859
    }
860
861
862
    /**
863
     * Make this page a child of another page.
864
     *
865
     * If the parent page does not exist, resolve it to a valid ID before updating this page's reference.
866
     *
867
     * @param SiteTree|int $item Either the parent object, or the parent ID
868
     */
869
    public function setParent($item)
870
    {
871
        if (is_object($item)) {
872
            if (!$item->exists()) {
873
                $item->write();
874
            }
875
            $this->setField("ParentID", $item->ID);
876
        } else {
877
            $this->setField("ParentID", $item);
878
        }
879
    }
880
881
    /**
882
     * Get the parent of this page.
883
     *
884
     * @return SiteTree Parent of this page
885
     */
886
    public function getParent()
887
    {
888
        if ($parentID = $this->getField("ParentID")) {
889
            return DataObject::get_by_id(self::class, $parentID);
890
        }
891
        return null;
892
    }
893
894
    /**
895
     * Return a string of the form "parent - page" or "grandparent - parent - page" using page titles
896
     *
897
     * @param int $level The maximum amount of levels to traverse.
898
     * @param string $separator Seperating string
899
     * @return string The resulting string
900
     */
901
    public function NestedTitle($level = 2, $separator = " - ")
902
    {
903
        $item = $this;
904
        $parts = [];
905
        while ($item && $level > 0) {
906
            $parts[] = $item->Title;
907
            $item = $item->getParent();
908
            $level--;
909
        }
910
        return implode($separator, array_reverse($parts));
911
    }
912
913
    /**
914
     * This function should return true if the current user can execute this action. It can be overloaded to customise
915
     * the security model for an application.
916
     *
917
     * Slightly altered from parent behaviour in {@link DataObject->can()}:
918
     * - Checks for existence of a method named "can<$perm>()" on the object
919
     * - Calls decorators and only returns for FALSE "vetoes"
920
     * - Falls back to {@link Permission::check()}
921
     * - Does NOT check for many-many relations named "Can<$perm>"
922
     *
923
     * @uses DataObjectDecorator->can()
924
     *
925
     * @param string $perm The permission to be checked, such as 'View'
926
     * @param Member $member The member whose permissions need checking. Defaults to the currently logged in user.
927
     * @param array $context Context argument for canCreate()
928
     * @return bool True if the the member is allowed to do the given action
929
     */
930
    public function can($perm, $member = null, $context = array())
931
    {
932
        if (!$member) {
933
            $member = Security::getCurrentUser();
934
        }
935
936
        if ($member && Permission::checkMember($member, "ADMIN")) {
937
            return true;
938
        }
939
940
        if (is_string($perm) && method_exists($this, 'can' . ucfirst($perm))) {
941
            $method = 'can' . ucfirst($perm);
942
            return $this->$method($member);
943
        }
944
945
        $results = $this->extend('can', $member);
946
        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...
947
            if (!min($results)) {
948
                return false;
949
            }
950
        }
951
952
        return ($member && Permission::checkMember($member, $perm));
953
    }
954
955
    /**
956
     * This function should return true if the current user can add children to this page. It can be overloaded to
957
     * customise the security model for an application.
958
     *
959
     * Denies permission if any of the following conditions is true:
960
     * - alternateCanAddChildren() on a extension returns false
961
     * - canEdit() is not granted
962
     * - There are no classes defined in {@link $allowed_children}
963
     *
964
     * @uses SiteTreeExtension->canAddChildren()
965
     * @uses canEdit()
966
     * @uses $allowed_children
967
     *
968
     * @param Member|int $member
969
     * @return bool True if the current user can add children
970
     */
971
    public function canAddChildren($member = null)
972
    {
973
        // Disable adding children to archived pages
974
        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...
975
            return false;
976
        }
977
978
        if (!$member) {
979
            $member = Security::getCurrentUser();
980
        }
981
982
        // Standard mechanism for accepting permission changes from extensions
983
        $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...
984
        if ($extended !== null) {
985
            return $extended;
986
        }
987
988
        // Default permissions
989
        if ($member && Permission::checkMember($member, "ADMIN")) {
990
            return true;
991
        }
992
993
        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 971 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...
994
    }
995
996
    /**
997
     * This function should return true if the current user can view this page. It can be overloaded to customise the
998
     * security model for an application.
999
     *
1000
     * Denies permission if any of the following conditions is true:
1001
     * - canView() on any extension returns false
1002
     * - "CanViewType" directive is set to "Inherit" and any parent page return false for canView()
1003
     * - "CanViewType" directive is set to "LoggedInUsers" and no user is logged in
1004
     * - "CanViewType" directive is set to "OnlyTheseUsers" and user is not in the given groups
1005
     *
1006
     * @uses DataExtension->canView()
1007
     * @uses ViewerGroups()
1008
     *
1009
     * @param Member $member
1010
     * @return bool True if the current user can view this page
1011
     */
1012
    public function canView($member = null)
1013
    {
1014
        if (!$member) {
1015
            $member = Security::getCurrentUser();
1016
        }
1017
1018
        // Standard mechanism for accepting permission changes from extensions
1019
        $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...
1020
        if ($extended !== null) {
1021
            return $extended;
1022
        }
1023
1024
        // admin override
1025
        if ($member && Permission::checkMember($member, array("ADMIN", "SITETREE_VIEW_ALL"))) {
1026
            return true;
1027
        }
1028
1029
        // Orphaned pages (in the current stage) are unavailable, except for admins via the CMS
1030
        if ($this->isOrphaned()) {
1031
            return false;
1032
        }
1033
1034
        // Note: getInheritedPermissions() is disused in this instance
1035
        // to allow parent canView extensions to influence subpage canView()
1036
1037
        // check for empty spec
1038
        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...
1039
            return true;
1040
        }
1041
1042
        // check for inherit
1043
        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...
1044
            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...
1045
                return $this->Parent()->canView($member);
1046
            } else {
1047
                return $this->getSiteConfig()->canViewPages($member);
1048
            }
1049
        }
1050
1051
        // check for any logged-in users
1052
        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...
1053
            return true;
1054
        }
1055
1056
        // check for specific groups
1057
        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...
1058
            && $member
1059
            && $member->inGroups($this->ViewerGroups())
1060
        ) {
1061
            return true;
1062
        }
1063
1064
        return false;
1065
    }
1066
1067
    /**
1068
     * Check if this page can be published
1069
     *
1070
     * @param Member $member
1071
     * @return bool
1072
     */
1073 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...
1074
    {
1075
        if (!$member) {
1076
            $member = Security::getCurrentUser();
1077
        }
1078
1079
        // Check extension
1080
        $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...
1081
        if ($extended !== null) {
1082
            return $extended;
1083
        }
1084
1085
        if (Permission::checkMember($member, "ADMIN")) {
1086
            return true;
1087
        }
1088
1089
        // Default to relying on edit permission
1090
        return $this->canEdit($member);
1091
    }
1092
1093
    /**
1094
     * This function should return true if the current user can delete this page. It can be overloaded to customise the
1095
     * security model for an application.
1096
     *
1097
     * Denies permission if any of the following conditions is true:
1098
     * - canDelete() returns false on any extension
1099
     * - canEdit() returns false
1100
     * - any descendant page returns false for canDelete()
1101
     *
1102
     * @uses canDelete()
1103
     * @uses SiteTreeExtension->canDelete()
1104
     * @uses canEdit()
1105
     *
1106
     * @param Member $member
1107
     * @return bool True if the current user can delete this page
1108
     */
1109
    public function canDelete($member = null)
1110
    {
1111
        if (!$member) {
1112
            $member = Security::getCurrentUser();
1113
        }
1114
1115
        // Standard mechanism for accepting permission changes from extensions
1116
        $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...
1117
        if ($extended !== null) {
1118
            return $extended;
1119
        }
1120
1121
        if (!$member) {
1122
            return false;
1123
        }
1124
1125
        // Default permission check
1126
        if (Permission::checkMember($member, array("ADMIN", "SITETREE_EDIT_ALL"))) {
1127
            return true;
1128
        }
1129
1130
        // Check inherited permissions
1131
        return static::getPermissionChecker()
1132
            ->canDelete($this->ID, $member);
1133
    }
1134
1135
    /**
1136
     * This function should return true if the current user can create new pages of this class, regardless of class. It
1137
     * can be overloaded to customise the security model for an application.
1138
     *
1139
     * By default, permission to create at the root level is based on the SiteConfig configuration, and permission to
1140
     * create beneath a parent is based on the ability to edit that parent page.
1141
     *
1142
     * Use {@link canAddChildren()} to control behaviour of creating children under this page.
1143
     *
1144
     * @uses $can_create
1145
     * @uses DataExtension->canCreate()
1146
     *
1147
     * @param Member $member
1148
     * @param array $context Optional array which may contain array('Parent' => $parentObj)
1149
     *                       If a parent page is known, it will be checked for validity.
1150
     *                       If omitted, it will be assumed this is to be created as a top level page.
1151
     * @return bool True if the current user can create pages on this class.
1152
     */
1153
    public function canCreate($member = null, $context = array())
1154
    {
1155
        if (!$member) {
1156
            $member = Security::getCurrentUser();
1157
        }
1158
1159
        // Check parent (custom canCreate option for SiteTree)
1160
        // Block children not allowed for this parent type
1161
        $parent = isset($context['Parent']) ? $context['Parent'] : null;
1162
        $strictParentInstance = ($parent && $parent instanceof SiteTree);
1163
        if ($strictParentInstance && !in_array(static::class, $parent->allowedChildren())) {
1164
            return false;
1165
        }
1166
1167
        // Standard mechanism for accepting permission changes from extensions
1168
        $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...
1169
        if ($extended !== null) {
1170
            return $extended;
1171
        }
1172
1173
        // Check permission
1174
        if ($member && Permission::checkMember($member, "ADMIN")) {
1175
            return true;
1176
        }
1177
1178
        // Fall over to inherited permissions
1179
        if ($strictParentInstance && $parent->exists()) {
1180
            return $parent->canAddChildren($member);
1181
        } else {
1182
            // This doesn't necessarily mean we are creating a root page, but that
1183
            // we don't know if there is a parent, so default to this permission
1184
            return SiteConfig::current_site_config()->canCreateTopLevel($member);
1185
        }
1186
    }
1187
1188
    /**
1189
     * This function should return true if the current user can edit this page. It can be overloaded to customise the
1190
     * security model for an application.
1191
     *
1192
     * Denies permission if any of the following conditions is true:
1193
     * - canEdit() on any extension returns false
1194
     * - canView() return false
1195
     * - "CanEditType" directive is set to "Inherit" and any parent page return false for canEdit()
1196
     * - "CanEditType" directive is set to "LoggedInUsers" and no user is logged in or doesn't have the
1197
     *   CMS_Access_CMSMAIN permission code
1198
     * - "CanEditType" directive is set to "OnlyTheseUsers" and user is not in the given groups
1199
     *
1200
     * @uses canView()
1201
     * @uses EditorGroups()
1202
     * @uses DataExtension->canEdit()
1203
     *
1204
     * @param Member $member Set to false if you want to explicitly test permissions without a valid user (useful for
1205
     *                       unit tests)
1206
     * @return bool True if the current user can edit this page
1207
     */
1208 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...
1209
    {
1210
        if (!$member) {
1211
            $member = Security::getCurrentUser();
1212
        }
1213
1214
        // Standard mechanism for accepting permission changes from extensions
1215
        $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...
1216
        if ($extended !== null) {
1217
            return $extended;
1218
        }
1219
1220
        // Default permissions
1221
        if (Permission::checkMember($member, "SITETREE_EDIT_ALL")) {
1222
            return true;
1223
        }
1224
1225
        // Check inherited permissions
1226
        return static::getPermissionChecker()
1227
            ->canEdit($this->ID, $member);
1228
    }
1229
1230
    /**
1231
     * Stub method to get the site config, unless the current class can provide an alternate.
1232
     *
1233
     * @return SiteConfig
1234
     */
1235
    public function getSiteConfig()
1236
    {
1237
        $configs = $this->invokeWithExtensions('alternateSiteConfig');
1238
        foreach (array_filter($configs) as $config) {
1239
            return $config;
1240
        }
1241
1242
        return SiteConfig::current_site_config();
1243
    }
1244
1245
    /**
1246
     * @return PermissionChecker
1247
     */
1248
    public static function getPermissionChecker()
1249
    {
1250
        return Injector::inst()->get(PermissionChecker::class.'.sitetree');
1251
    }
1252
1253
    /**
1254
     * Collate selected descendants of this page.
1255
     *
1256
     * {@link $condition} will be evaluated on each descendant, and if it is succeeds, that item will be added to the
1257
     * $collator array.
1258
     *
1259
     * @param string $condition The PHP condition to be evaluated. The page will be called $item
1260
     * @param array  $collator  An array, passed by reference, to collect all of the matching descendants.
1261
     * @return bool
1262
     */
1263
    public function collateDescendants($condition, &$collator)
1264
    {
1265
        // apply reasonable hierarchy limits
1266
        $threshold = Config::inst()->get(Hierarchy::class, 'node_threshold_leaf');
1267
        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...
1268
            return false;
1269
        }
1270
1271
        $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...
1272
        if ($children) {
1273
            foreach ($children as $item) {
1274
                if (eval("return $condition;")) {
1275
                    $collator[] = $item;
1276
                }
1277
                /** @var SiteTree $item */
1278
                $item->collateDescendants($condition, $collator);
1279
            }
1280
            return true;
1281
        }
1282
        return false;
1283
    }
1284
1285
    /**
1286
     * Return the title, description, keywords and language metatags.
1287
     *
1288
     * @todo Move <title> tag in separate getter for easier customization and more obvious usage
1289
     *
1290
     * @param bool $includeTitle Show default <title>-tag, set to false for custom templating
1291
     * @return string The XHTML metatags
1292
     */
1293
    public function MetaTags($includeTitle = true)
1294
    {
1295
        $tags = array();
1296
        if ($includeTitle && strtolower($includeTitle) != 'false') {
1297
            $tags[] = HTML::createTag('title', array(), $this->obj('Title')->forTemplate());
1298
        }
1299
1300
        $generator = trim(Config::inst()->get(self::class, 'meta_generator'));
1301
        if (!empty($generator)) {
1302
            $tags[] = HTML::createTag('meta', array(
1303
                'name' => 'generator',
1304
                'content' => $generator,
1305
            ));
1306
        }
1307
1308
        $charset = ContentNegotiator::config()->uninherited('encoding');
1309
        $tags[] = HTML::createTag('meta', array(
1310
            'http-equiv' => 'Content-Type',
1311
            'content' => 'text/html; charset=' . $charset,
1312
        ));
1313
        if ($this->MetaDescription) {
1314
            $tags[] = HTML::createTag('meta', array(
1315
                'name' => 'description',
1316
                'content' => $this->MetaDescription,
1317
            ));
1318
        }
1319
1320
        if (Permission::check('CMS_ACCESS_CMSMain')
1321
            && $this->ID > 0
1322
        ) {
1323
            $tags[] = HTML::createTag('meta', array(
1324
                'name' => 'x-page-id',
1325
                'content' => $this->obj('ID')->forTemplate(),
1326
            ));
1327
            $tags[] = HTML::createTag('meta', array(
1328
                'name' => 'x-cms-edit-link',
1329
                'content' => $this->obj('CMSEditLink')->forTemplate(),
1330
            ));
1331
        }
1332
1333
        $tags = implode("\n", $tags);
1334
        if ($this->ExtraMeta) {
1335
            $tags .= $this->obj('ExtraMeta')->forTemplate();
1336
        }
1337
1338
        $this->extend('MetaTags', $tags);
1339
1340
        return $tags;
1341
    }
1342
1343
    /**
1344
     * Returns the object that contains the content that a user would associate with this page.
1345
     *
1346
     * Ordinarily, this is just the page itself, but for example on RedirectorPages or VirtualPages ContentSource() will
1347
     * return the page that is linked to.
1348
     *
1349
     * @return $this
1350
     */
1351
    public function ContentSource()
1352
    {
1353
        return $this;
1354
    }
1355
1356
    /**
1357
     * Add default records to database.
1358
     *
1359
     * This function is called whenever the database is built, after the database tables have all been created. Overload
1360
     * this to add default records when the database is built, but make sure you call parent::requireDefaultRecords().
1361
     */
1362
    public function requireDefaultRecords()
1363
    {
1364
        parent::requireDefaultRecords();
1365
1366
        // default pages
1367
        if (static::class == self::class && $this->config()->create_default_pages) {
1368
            if (!SiteTree::get_by_link(RootURLController::config()->default_homepage_link)) {
1369
                $homepage = new Page();
1370
                $homepage->Title = _t(__CLASS__.'.DEFAULTHOMETITLE', 'Home');
1371
                $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>');
1372
                $homepage->URLSegment = RootURLController::config()->default_homepage_link;
1373
                $homepage->Sort = 1;
1374
                $homepage->write();
1375
                $homepage->copyVersionToStage(Versioned::DRAFT, Versioned::LIVE);
1376
                $homepage->flushCache();
1377
                DB::alteration_message('Home page created', 'created');
1378
            }
1379
1380
            if (DB::query("SELECT COUNT(*) FROM \"SiteTree\"")->value() == 1) {
1381
                $aboutus = new Page();
1382
                $aboutus->Title = _t(__CLASS__.'.DEFAULTABOUTTITLE', 'About Us');
1383
                $aboutus->Content = _t(
1384
                    'SilverStripe\\CMS\\Model\\SiteTree.DEFAULTABOUTCONTENT',
1385
                    '<p>You can fill this page out with your own content, or delete it and create your own pages.</p>'
1386
                );
1387
                $aboutus->Sort = 2;
1388
                $aboutus->write();
1389
                $aboutus->copyVersionToStage(Versioned::DRAFT, Versioned::LIVE);
1390
                $aboutus->flushCache();
1391
                DB::alteration_message('About Us page created', 'created');
1392
1393
                $contactus = new Page();
1394
                $contactus->Title = _t(__CLASS__.'.DEFAULTCONTACTTITLE', 'Contact Us');
1395
                $contactus->Content = _t(
1396
                    'SilverStripe\\CMS\\Model\\SiteTree.DEFAULTCONTACTCONTENT',
1397
                    '<p>You can fill this page out with your own content, or delete it and create your own pages.</p>'
1398
                );
1399
                $contactus->Sort = 3;
1400
                $contactus->write();
1401
                $contactus->copyVersionToStage(Versioned::DRAFT, Versioned::LIVE);
1402
                $contactus->flushCache();
1403
                DB::alteration_message('Contact Us page created', 'created');
1404
            }
1405
        }
1406
    }
1407
1408
    protected function onBeforeWrite()
1409
    {
1410
        parent::onBeforeWrite();
1411
1412
        // If Sort hasn't been set, make this page come after it's siblings
1413
        if (!$this->Sort) {
1414
            $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...
1415
            $this->Sort = DB::prepared_query(
1416
                "SELECT MAX(\"Sort\") + 1 FROM \"SiteTree\" WHERE \"ParentID\" = ?",
1417
                array($parentID)
1418
            )->value();
1419
        }
1420
1421
        // If there is no URLSegment set, generate one from Title
1422
        $defaultSegment = $this->generateURLSegment(_t(
1423
            'SilverStripe\\CMS\\Controllers\\CMSMain.NEWPAGE',
1424
            'New {pagetype}',
1425
            array('pagetype' => $this->i18n_singular_name())
1426
        ));
1427
        if ((!$this->URLSegment || $this->URLSegment == $defaultSegment) && $this->Title) {
1428
            $this->URLSegment = $this->generateURLSegment($this->Title);
1429
        } elseif ($this->isChanged('URLSegment', 2)) {
1430
            // Do a strict check on change level, to avoid double encoding caused by
1431
            // bogus changes through forceChange()
1432
            $filter = URLSegmentFilter::create();
1433
            $this->URLSegment = $filter->filter($this->URLSegment);
1434
            // If after sanitising there is no URLSegment, give it a reasonable default
1435
            if (!$this->URLSegment) {
1436
                $this->URLSegment = "page-$this->ID";
1437
            }
1438
        }
1439
1440
        // Ensure that this object has a non-conflicting URLSegment value.
1441
        $count = 2;
1442
        while (!$this->validURLSegment()) {
1443
            $this->URLSegment = preg_replace('/-[0-9]+$/', null, $this->URLSegment) . '-' . $count;
1444
            $count++;
1445
        }
1446
1447
        $this->syncLinkTracking();
1448
1449
        // Check to see if we've only altered fields that shouldn't affect versioning
1450
        $fieldsIgnoredByVersioning = array('HasBrokenLink', 'Status', 'HasBrokenFile', 'ToDo', 'VersionID', 'SaveCount');
1451
        $changedFields = array_keys($this->getChangedFields(true, 2));
1452
1453
        // This more rigorous check is inline with the test that write() does to decide whether or not to write to the
1454
        // DB. We use that to avoid cluttering the system with a migrateVersion() call that doesn't get used
1455
        $oneChangedFields = array_keys($this->getChangedFields(true, 1));
1456
1457
        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...
1458
            // This will have the affect of preserving the versioning
1459
            $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...
1460
        }
1461
    }
1462
1463
    /**
1464
     * Trigger synchronisation of link tracking
1465
     *
1466
     * {@see SiteTreeLinkTracking::augmentSyncLinkTracking}
1467
     */
1468
    public function syncLinkTracking()
1469
    {
1470
        $this->extend('augmentSyncLinkTracking');
1471
    }
1472
1473
    public function onBeforeDelete()
1474
    {
1475
        parent::onBeforeDelete();
1476
1477
        // If deleting this page, delete all its children.
1478
        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...
1479
            foreach ($children as $child) {
1480
                /** @var SiteTree $child */
1481
                $child->delete();
1482
            }
1483
        }
1484
    }
1485
1486
    public function onAfterDelete()
1487
    {
1488
        $this->updateDependentPages();
1489
        parent::onAfterDelete();
1490
    }
1491
1492
    public function flushCache($persistent = true)
1493
    {
1494
        parent::flushCache($persistent);
1495
        $this->_cache_statusFlags = null;
1496
    }
1497
1498
    public function validate()
1499
    {
1500
        $result = parent::validate();
1501
1502
        // Allowed children validation
1503
        $parent = $this->getParent();
1504
        if ($parent && $parent->exists()) {
1505
            // No need to check for subclasses or instanceof, as allowedChildren() already
1506
            // deconstructs any inheritance trees already.
1507
            $allowed = $parent->allowedChildren();
1508
            $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...
1509
                ? $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...
1510
                : $this;
1511
            if (!in_array($subject->ClassName, $allowed)) {
1512
                $result->addError(
1513
                    _t(
1514
                        'SilverStripe\\CMS\\Model\\SiteTree.PageTypeNotAllowed',
1515
                        'Page type "{type}" not allowed as child of this parent page',
1516
                        array('type' => $subject->i18n_singular_name())
1517
                    ),
1518
                    ValidationResult::TYPE_ERROR,
1519
                    'ALLOWED_CHILDREN'
1520
                );
1521
            }
1522
        }
1523
1524
        // "Can be root" validation
1525
        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...
1526
            $result->addError(
1527
                _t(
1528
                    'SilverStripe\\CMS\\Model\\SiteTree.PageTypNotAllowedOnRoot',
1529
                    'Page type "{type}" is not allowed on the root level',
1530
                    array('type' => $this->i18n_singular_name())
1531
                ),
1532
                ValidationResult::TYPE_ERROR,
1533
                'CAN_BE_ROOT'
1534
            );
1535
        }
1536
1537
        return $result;
1538
    }
1539
1540
    /**
1541
     * Returns true if this object has a URLSegment value that does not conflict with any other objects. This method
1542
     * checks for:
1543
     *  - A page with the same URLSegment that has a conflict
1544
     *  - Conflicts with actions on the parent page
1545
     *  - A conflict caused by a root page having the same URLSegment as a class name
1546
     *
1547
     * @return bool
1548
     */
1549
    public function validURLSegment()
1550
    {
1551
        if (self::config()->nested_urls && $parent = $this->Parent()) {
1552
            if ($controller = ModelAsController::controller_for($parent)) {
1553
                if ($controller instanceof Controller && $controller->hasAction($this->URLSegment)) {
1554
                    return false;
1555
                }
1556
            }
1557
        }
1558
1559
        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...
1560
            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...
1561
                return false;
1562
            }
1563
        }
1564
1565
        // Filters by url, id, and parent
1566
        $filter = array('"SiteTree"."URLSegment"' => $this->URLSegment);
1567
        if ($this->ID) {
1568
            $filter['"SiteTree"."ID" <> ?'] = $this->ID;
1569
        }
1570
        if (self::config()->nested_urls) {
1571
            $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...
1572
        }
1573
1574
        // If any of the extensions return `0` consider the segment invalid
1575
        $extensionResponses = array_filter(
1576
            (array)$this->extend('augmentValidURLSegment'),
1577
            function ($response) {
1578
                return !is_null($response);
1579
            }
1580
        );
1581
        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...
1582
            return min($extensionResponses);
1583
        }
1584
1585
        // Check existence
1586
        return !DataObject::get(self::class, $filter)->exists();
1587
    }
1588
1589
    /**
1590
     * Generate a URL segment based on the title provided.
1591
     *
1592
     * If {@link Extension}s wish to alter URL segment generation, they can do so by defining
1593
     * updateURLSegment(&$url, $title).  $url will be passed by reference and should be modified. $title will contain
1594
     * the title that was originally used as the source of this generated URL. This lets extensions either start from
1595
     * scratch, or incrementally modify the generated URL.
1596
     *
1597
     * @param string $title Page title
1598
     * @return string Generated url segment
1599
     */
1600
    public function generateURLSegment($title)
1601
    {
1602
        $filter = URLSegmentFilter::create();
1603
        $t = $filter->filter($title);
1604
1605
        // Fallback to generic page name if path is empty (= no valid, convertable characters)
1606
        if (!$t || $t == '-' || $t == '-1') {
1607
            $t = "page-$this->ID";
1608
        }
1609
1610
        // Hook for extensions
1611
        $this->extend('updateURLSegment', $t, $title);
1612
1613
        return $t;
1614
    }
1615
1616
    /**
1617
     * Gets the URL segment for the latest draft version of this page.
1618
     *
1619
     * @return string
1620
     */
1621
    public function getStageURLSegment()
1622
    {
1623
        $stageRecord = Versioned::get_one_by_stage(self::class, Versioned::DRAFT, array(
1624
            '"SiteTree"."ID"' => $this->ID
1625
        ));
1626
        return ($stageRecord) ? $stageRecord->URLSegment : null;
1627
    }
1628
1629
    /**
1630
     * Gets the URL segment for the currently published version of this page.
1631
     *
1632
     * @return string
1633
     */
1634
    public function getLiveURLSegment()
1635
    {
1636
        $liveRecord = Versioned::get_one_by_stage(self::class, Versioned::LIVE, array(
1637
            '"SiteTree"."ID"' => $this->ID
1638
        ));
1639
        return ($liveRecord) ? $liveRecord->URLSegment : null;
1640
    }
1641
1642
    /**
1643
     * Returns the pages that depend on this page. This includes virtual pages, pages that link to it, etc.
1644
     *
1645
     * @param bool $includeVirtuals Set to false to exlcude virtual pages.
1646
     * @return ArrayList
1647
     */
1648
    public function DependentPages($includeVirtuals = true)
1649
    {
1650
        if (class_exists('Subsite')) {
1651
            $origDisableSubsiteFilter = Subsite::$disable_subsite_filter;
1652
            Subsite::disable_subsite_filter(true);
1653
        }
1654
1655
        // Content links
1656
        $items = new ArrayList();
1657
1658
        // We merge all into a regular SS_List, because DataList doesn't support merge
1659
        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...
1660
            $linkList = new ArrayList();
1661
            foreach ($contentLinks as $item) {
1662
                $item->DependentLinkType = 'Content link';
1663
                $linkList->push($item);
1664
            }
1665
            $items->merge($linkList);
1666
        }
1667
1668
        // Virtual pages
1669
        if ($includeVirtuals) {
1670
            $virtuals = $this->VirtualPages();
1671
            if ($virtuals) {
1672
                $virtualList = new ArrayList();
1673
                foreach ($virtuals as $item) {
1674
                    $item->DependentLinkType = 'Virtual page';
1675
                    $virtualList->push($item);
1676
                }
1677
                $items->merge($virtualList);
1678
            }
1679
        }
1680
1681
        // Redirector pages
1682
        $redirectors = RedirectorPage::get()->where(array(
1683
            '"RedirectorPage"."RedirectionType"' => 'Internal',
1684
            '"RedirectorPage"."LinkToID"' => $this->ID
1685
        ));
1686
        if ($redirectors) {
1687
            $redirectorList = new ArrayList();
1688
            foreach ($redirectors as $item) {
1689
                $item->DependentLinkType = 'Redirector page';
1690
                $redirectorList->push($item);
1691
            }
1692
            $items->merge($redirectorList);
1693
        }
1694
1695
        if (class_exists('Subsite')) {
1696
            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...
1697
        }
1698
1699
        return $items;
1700
    }
1701
1702
    /**
1703
     * Return all virtual pages that link to this page.
1704
     *
1705
     * @return DataList
1706
     */
1707
    public function VirtualPages()
1708
    {
1709
        $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...
1710
1711
        // Disable subsite filter for these pages
1712
        if ($pages instanceof DataList) {
1713
            return $pages->setDataQueryParam('Subsite.filter', false);
1714
        } else {
1715
            return $pages;
1716
        }
1717
    }
1718
1719
    /**
1720
     * Returns a FieldList with which to create the main editing form.
1721
     *
1722
     * You can override this in your child classes to add extra fields - first get the parent fields using
1723
     * parent::getCMSFields(), then use addFieldToTab() on the FieldList.
1724
     *
1725
     * See {@link getSettingsFields()} for a different set of fields concerned with configuration aspects on the record,
1726
     * e.g. access control.
1727
     *
1728
     * @return FieldList The fields to be displayed in the CMS
1729
     */
1730
    public function getCMSFields()
1731
    {
1732
        // Status / message
1733
        // Create a status message for multiple parents
1734
        if ($this->ID && is_numeric($this->ID)) {
1735
            $linkedPages = $this->VirtualPages();
1736
1737
            $parentPageLinks = array();
1738
1739
            if ($linkedPages->count() > 0) {
1740
                /** @var VirtualPage $linkedPage */
1741
                foreach ($linkedPages as $linkedPage) {
1742
                    $parentPage = $linkedPage->Parent();
1743
                    if ($parentPage && $parentPage->exists()) {
1744
                        $link = Convert::raw2att($parentPage->CMSEditLink());
1745
                        $title = Convert::raw2xml($parentPage->Title);
1746
                    } else {
1747
                        $link = CMSPageEditController::singleton()->Link('show');
1748
                        $title = _t(__CLASS__.'.TOPLEVEL', 'Site Content (Top Level)');
1749
                    }
1750
                    $parentPageLinks[] = "<a class=\"cmsEditlink\" href=\"{$link}\">{$title}</a>";
1751
                }
1752
1753
                $lastParent = array_pop($parentPageLinks);
1754
                $parentList = "'$lastParent'";
1755
1756
                if (count($parentPageLinks)) {
1757
                    $parentList = "'" . implode("', '", $parentPageLinks) . "' and "
1758
                        . $parentList;
1759
                }
1760
1761
                $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...
1762
                    'SilverStripe\\CMS\\Model\\SiteTree.APPEARSVIRTUALPAGES',
1763
                    "This content also appears on the virtual pages in the {title} sections.",
1764
                    array('title' => $parentList)
1765
                );
1766
            }
1767
        }
1768
1769
        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...
1770
            $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...
1771
        }
1772
1773
        $dependentNote = '';
1774
        $dependentTable = new LiteralField('DependentNote', '<p></p>');
1775
1776
        // Create a table for showing pages linked to this one
1777
        $dependentPages = $this->DependentPages();
1778
        $dependentPagesCount = $dependentPages->count();
1779
        if ($dependentPagesCount) {
1780
            $dependentColumns = array(
1781
                'Title' => $this->fieldLabel('Title'),
1782
                'AbsoluteLink' => _t(__CLASS__.'.DependtPageColumnURL', 'URL'),
1783
                'DependentLinkType' => _t(__CLASS__.'.DependtPageColumnLinkType', 'Link type'),
1784
            );
1785
            if (class_exists('Subsite')) {
1786
                $dependentColumns['Subsite.Title'] = singleton('Subsite')->i18n_singular_name();
1787
            }
1788
1789
            $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>');
1790
            $dependentTable = GridField::create(
1791
                'DependentPages',
1792
                false,
1793
                $dependentPages
1794
            );
1795
            /** @var GridFieldDataColumns $dataColumns */
1796
            $dataColumns = $dependentTable->getConfig()->getComponentByType('SilverStripe\\Forms\\GridField\\GridFieldDataColumns');
1797
            $dataColumns
1798
                ->setDisplayFields($dependentColumns)
1799
                ->setFieldFormatting(array(
1800
                    'Title' => function ($value, &$item) {
1801
                        return sprintf(
1802
                            '<a href="admin/pages/edit/show/%d">%s</a>',
1803
                            (int)$item->ID,
1804
                            Convert::raw2xml($item->Title)
1805
                        );
1806
                    },
1807
                    '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...
1808
                        return sprintf(
1809
                            '<a href="%s" target="_blank">%s</a>',
1810
                            Convert::raw2xml($value),
1811
                            Convert::raw2xml($value)
1812
                        );
1813
                    }
1814
                ));
1815
        }
1816
1817
        $baseLink = Controller::join_links(
1818
            Director::absoluteBaseURL(),
1819
            (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...
1820
        );
1821
1822
        $urlsegment = SiteTreeURLSegmentField::create("URLSegment", $this->fieldLabel('URLSegment'))
1823
            ->setURLPrefix($baseLink)
1824
            ->setDefaultURL($this->generateURLSegment(_t(
1825
                'SilverStripe\\CMS\\Controllers\\CMSMain.NEWPAGE',
1826
                'New {pagetype}',
1827
                array('pagetype' => $this->i18n_singular_name())
1828
            )));
1829
        $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...
1830
            ? $this->fieldLabel('LinkChangeNote')
1831
            : '';
1832
        if (!Config::inst()->get('SilverStripe\\View\\Parsers\\URLSegmentFilter', 'default_allow_multibyte')) {
1833
            $helpText .= _t('SilverStripe\\CMS\\Forms\\SiteTreeURLSegmentField.HelpChars', ' Special characters are automatically converted or removed.');
1834
        }
1835
        $urlsegment->setHelpText($helpText);
1836
1837
        $fields = new FieldList(
1838
            $rootTab = new TabSet(
1839
                "Root",
1840
                $tabMain = new Tab(
1841
                    'Main',
1842
                    new TextField("Title", $this->fieldLabel('Title')),
1843
                    $urlsegment,
1844
                    new TextField("MenuTitle", $this->fieldLabel('MenuTitle')),
1845
                    $htmlField = new HTMLEditorField("Content", _t(__CLASS__.'.HTMLEDITORTITLE', "Content", 'HTML editor title')),
1846
                    ToggleCompositeField::create(
1847
                        'Metadata',
1848
                        _t(__CLASS__.'.MetadataToggle', 'Metadata'),
1849
                        array(
1850
                            $metaFieldDesc = new TextareaField("MetaDescription", $this->fieldLabel('MetaDescription')),
1851
                            $metaFieldExtra = new TextareaField("ExtraMeta", $this->fieldLabel('ExtraMeta'))
1852
                        )
1853
                    )->setHeadingLevel(4)
1854
                ),
1855
                $tabDependent = new Tab(
1856
                    'Dependent',
1857
                    $dependentNote,
1858
                    $dependentTable
1859
                )
1860
            )
1861
        );
1862
        $htmlField->addExtraClass('stacked');
1863
1864
        // Help text for MetaData on page content editor
1865
        $metaFieldDesc
1866
            ->setRightTitle(
1867
                _t(
1868
                    'SilverStripe\\CMS\\Model\\SiteTree.METADESCHELP',
1869
                    "Search engines use this content for displaying search results (although it will not influence their ranking)."
1870
                )
1871
            )
1872
            ->addExtraClass('help');
1873
        $metaFieldExtra
1874
            ->setRightTitle(
1875
                _t(
1876
                    'SilverStripe\\CMS\\Model\\SiteTree.METAEXTRAHELP',
1877
                    "HTML tags for additional meta information. For example &lt;meta name=\"customName\" content=\"your custom content here\" /&gt;"
1878
                )
1879
            )
1880
            ->addExtraClass('help');
1881
1882
        // Conditional dependent pages tab
1883
        if ($dependentPagesCount) {
1884
            $tabDependent->setTitle(_t(__CLASS__.'.TABDEPENDENT', "Dependent pages") . " ($dependentPagesCount)");
1885
        } else {
1886
            $fields->removeFieldFromTab('Root', 'Dependent');
1887
        }
1888
1889
        $tabMain->setTitle(_t(__CLASS__.'.TABCONTENT', "Main Content"));
1890
1891
        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...
1892
            $obsoleteWarning = _t(
1893
                'SilverStripe\\CMS\\Model\\SiteTree.OBSOLETECLASS',
1894
                "This page is of obsolete type {type}. Saving will reset its type and you may lose data",
1895
                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...
1896
            );
1897
1898
            $fields->addFieldToTab(
1899
                "Root.Main",
1900
                new LiteralField("ObsoleteWarningHeader", "<p class=\"message warning\">$obsoleteWarning</p>"),
1901
                "Title"
1902
            );
1903
        }
1904
1905
        if (file_exists(BASE_PATH . '/install.php')) {
1906
            $fields->addFieldToTab("Root.Main", new LiteralField(
1907
                "InstallWarningHeader",
1908
                "<p class=\"message warning\">" . _t(
1909
                    "SilverStripe\\CMS\\Model\\SiteTree.REMOVE_INSTALL_WARNING",
1910
                    "Warning: You should remove install.php from this SilverStripe install for security reasons."
1911
                )
1912
                . "</p>"
1913
            ), "Title");
1914
        }
1915
1916
        if (self::$runCMSFieldsExtensions) {
1917
            $this->extend('updateCMSFields', $fields);
1918
        }
1919
1920
        return $fields;
1921
    }
1922
1923
1924
    /**
1925
     * Returns fields related to configuration aspects on this record, e.g. access control. See {@link getCMSFields()}
1926
     * for content-related fields.
1927
     *
1928
     * @return FieldList
1929
     */
1930
    public function getSettingsFields()
1931
    {
1932
        $mapFn = function ($groups = []) {
1933
            $map = [];
1934
            foreach ($groups as $group) {
1935
                // Listboxfield values are escaped, use ASCII char instead of &raquo;
1936
                $map[$group->ID] = $group->getBreadcrumbs(' > ');
1937
            }
1938
            asort($map);
1939
            return $map;
1940
        };
1941
        $groupsMap = $mapFn(Group::get());
1942
        $viewAllGroupsMap = $mapFn(Permission::get_groups_by_permission(['SITETREE_VIEW_ALL', 'ADMIN']));
1943
        $editAllGroupsMap = $mapFn(Permission::get_groups_by_permission(['SITETREE_EDIT_ALL', 'ADMIN']));
1944
1945
        $fields = new FieldList(
1946
            $rootTab = new TabSet(
1947
                "Root",
1948
                $tabBehaviour = new Tab(
1949
                    'Settings',
1950
                    new DropdownField(
1951
                        "ClassName",
1952
                        $this->fieldLabel('ClassName'),
1953
                        $this->getClassDropdown()
1954
                    ),
1955
                    $parentTypeSelector = new CompositeField(
1956
                        $parentType = new OptionsetField("ParentType", _t("SilverStripe\\CMS\\Model\\SiteTree.PAGELOCATION", "Page location"), array(
1957
                            "root" => _t("SilverStripe\\CMS\\Model\\SiteTree.PARENTTYPE_ROOT", "Top-level page"),
1958
                            "subpage" => _t("SilverStripe\\CMS\\Model\\SiteTree.PARENTTYPE_SUBPAGE", "Sub-page underneath a parent page"),
1959
                        )),
1960
                        $parentIDField = new TreeDropdownField("ParentID", $this->fieldLabel('ParentID'), self::class, 'ID', 'MenuTitle')
1961
                    ),
1962
                    $visibility = new FieldGroup(
1963
                        new CheckboxField("ShowInMenus", $this->fieldLabel('ShowInMenus')),
1964
                        new CheckboxField("ShowInSearch", $this->fieldLabel('ShowInSearch'))
1965
                    ),
1966
                    $viewersOptionsField = new OptionsetField(
1967
                        "CanViewType",
1968
                        _t(__CLASS__.'.ACCESSHEADER', "Who can view this page?")
1969
                    ),
1970
                    $viewerGroupsField = ListboxField::create("ViewerGroups", _t(__CLASS__.'.VIEWERGROUPS', "Viewer Groups"))
1971
                        ->setSource($groupsMap)
1972
                        ->setAttribute(
1973
                            'data-placeholder',
1974
                            _t(__CLASS__.'.GroupPlaceholder', 'Click to select group')
1975
                        ),
1976
                    $editorsOptionsField = new OptionsetField(
1977
                        "CanEditType",
1978
                        _t(__CLASS__.'.EDITHEADER', "Who can edit this page?")
1979
                    ),
1980
                    $editorGroupsField = ListboxField::create("EditorGroups", _t(__CLASS__.'.EDITORGROUPS', "Editor Groups"))
1981
                        ->setSource($groupsMap)
1982
                        ->setAttribute(
1983
                            'data-placeholder',
1984
                            _t(__CLASS__.'.GroupPlaceholder', 'Click to select group')
1985
                        )
1986
                )
1987
            )
1988
        );
1989
1990
        $parentType->addExtraClass('noborder');
1991
        $visibility->setTitle($this->fieldLabel('Visibility'));
1992
1993
1994
        // This filter ensures that the ParentID dropdown selection does not show this node,
1995
        // or its descendents, as this causes vanishing bugs
1996
        $parentIDField->setFilterFunction(function ($node) {
1997
            return $node->ID != $this->ID;
1998
        });
1999
        $parentTypeSelector->addExtraClass('parentTypeSelector');
2000
2001
        $tabBehaviour->setTitle(_t(__CLASS__.'.TABBEHAVIOUR', "Behavior"));
2002
2003
        // Make page location fields read-only if the user doesn't have the appropriate permission
2004
        if (!Permission::check("SITETREE_REORGANISE")) {
2005
            $fields->makeFieldReadonly('ParentType');
2006
            if ($this->getParentType() === 'root') {
2007
                $fields->removeByName('ParentID');
2008
            } else {
2009
                $fields->makeFieldReadonly('ParentID');
2010
            }
2011
        }
2012
2013
        $viewersOptionsSource = [
2014
            InheritedPermissions::INHERIT => _t(__CLASS__.'.INHERIT', "Inherit from parent page"),
2015
            InheritedPermissions::ANYONE => _t(__CLASS__.'.ACCESSANYONE', "Anyone"),
2016
            InheritedPermissions::LOGGED_IN_USERS => _t(__CLASS__.'.ACCESSLOGGEDIN', "Logged-in users"),
2017
            InheritedPermissions::ONLY_THESE_USERS => _t(
2018
                __CLASS__.'.ACCESSONLYTHESE',
2019
                "Only these groups (choose from list)"
2020
            ),
2021
        ];
2022
        $viewersOptionsField->setSource($viewersOptionsSource);
2023
2024
        // Editors have same options, except no "Anyone"
2025
        $editorsOptionsSource = $viewersOptionsSource;
2026
        unset($editorsOptionsSource[InheritedPermissions::ANYONE]);
2027
        $editorsOptionsField->setSource($editorsOptionsSource);
2028
2029
        if ($viewAllGroupsMap) {
2030
            $viewerGroupsField->setDescription(_t(
2031
                'SilverStripe\\CMS\\Model\\SiteTree.VIEWER_GROUPS_FIELD_DESC',
2032
                'Groups with global view permissions: {groupList}',
2033
                ['groupList' => implode(', ', array_values($viewAllGroupsMap))]
2034
            ));
2035
        }
2036
2037
        if ($editAllGroupsMap) {
2038
            $editorGroupsField->setDescription(_t(
2039
                'SilverStripe\\CMS\\Model\\SiteTree.EDITOR_GROUPS_FIELD_DESC',
2040
                'Groups with global edit permissions: {groupList}',
2041
                ['groupList' => implode(', ', array_values($editAllGroupsMap))]
2042
            ));
2043
        }
2044
2045
        if (!Permission::check('SITETREE_GRANT_ACCESS')) {
2046
            $fields->makeFieldReadonly($viewersOptionsField);
2047
            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...
2048
                $fields->makeFieldReadonly($viewerGroupsField);
2049
            } else {
2050
                $fields->removeByName('ViewerGroups');
2051
            }
2052
2053
            $fields->makeFieldReadonly($editorsOptionsField);
2054
            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...
2055
                $fields->makeFieldReadonly($editorGroupsField);
2056
            } else {
2057
                $fields->removeByName('EditorGroups');
2058
            }
2059
        }
2060
2061
        if (self::$runCMSFieldsExtensions) {
2062
            $this->extend('updateSettingsFields', $fields);
2063
        }
2064
2065
        return $fields;
2066
    }
2067
2068
    /**
2069
     * @param bool $includerelations A boolean value to indicate if the labels returned should include relation fields
2070
     * @return array
2071
     */
2072
    public function fieldLabels($includerelations = true)
2073
    {
2074
        $cacheKey = static::class . '_' . $includerelations;
2075
        if (!isset(self::$_cache_field_labels[$cacheKey])) {
2076
            $labels = parent::fieldLabels($includerelations);
2077
            $labels['Title'] = _t(__CLASS__.'.PAGETITLE', "Page name");
2078
            $labels['MenuTitle'] = _t(__CLASS__.'.MENUTITLE', "Navigation label");
2079
            $labels['MetaDescription'] = _t(__CLASS__.'.METADESC', "Meta Description");
2080
            $labels['ExtraMeta'] = _t(__CLASS__.'.METAEXTRA', "Custom Meta Tags");
2081
            $labels['ClassName'] = _t(__CLASS__.'.PAGETYPE', "Page type", 'Classname of a page object');
2082
            $labels['ParentType'] = _t(__CLASS__.'.PARENTTYPE', "Page location");
2083
            $labels['ParentID'] = _t(__CLASS__.'.PARENTID', "Parent page");
2084
            $labels['ShowInMenus'] =_t(__CLASS__.'.SHOWINMENUS', "Show in menus?");
2085
            $labels['ShowInSearch'] = _t(__CLASS__.'.SHOWINSEARCH', "Show in search?");
2086
            $labels['ViewerGroups'] = _t(__CLASS__.'.VIEWERGROUPS', "Viewer Groups");
2087
            $labels['EditorGroups'] = _t(__CLASS__.'.EDITORGROUPS', "Editor Groups");
2088
            $labels['URLSegment'] = _t(__CLASS__.'.URLSegment', 'URL Segment', 'URL for this page');
2089
            $labels['Content'] = _t(__CLASS__.'.Content', 'Content', 'Main HTML Content for a page');
2090
            $labels['CanViewType'] = _t(__CLASS__.'.Viewers', 'Viewers Groups');
2091
            $labels['CanEditType'] = _t(__CLASS__.'.Editors', 'Editors Groups');
2092
            $labels['Comments'] = _t(__CLASS__.'.Comments', 'Comments');
2093
            $labels['Visibility'] = _t(__CLASS__.'.Visibility', 'Visibility');
2094
            $labels['LinkChangeNote'] = _t(
2095
                'SilverStripe\\CMS\\Model\\SiteTree.LINKCHANGENOTE',
2096
                'Changing this page\'s link will also affect the links of all child pages.'
2097
            );
2098
2099
            if ($includerelations) {
2100
                $labels['Parent'] = _t(__CLASS__.'.has_one_Parent', 'Parent Page', 'The parent page in the site hierarchy');
2101
                $labels['LinkTracking'] = _t(__CLASS__.'.many_many_LinkTracking', 'Link Tracking');
2102
                $labels['ImageTracking'] = _t(__CLASS__.'.many_many_ImageTracking', 'Image Tracking');
2103
                $labels['BackLinkTracking'] = _t(__CLASS__.'.many_many_BackLinkTracking', 'Backlink Tracking');
2104
            }
2105
2106
            self::$_cache_field_labels[$cacheKey] = $labels;
2107
        }
2108
2109
        return self::$_cache_field_labels[$cacheKey];
2110
    }
2111
2112
    /**
2113
     * Get the actions available in the CMS for this page - eg Save, Publish.
2114
     *
2115
     * Frontend scripts and styles know how to handle the following FormFields:
2116
     * - top-level FormActions appear as standalone buttons
2117
     * - top-level CompositeField with FormActions within appear as grouped buttons
2118
     * - TabSet & Tabs appear as a drop ups
2119
     * - FormActions within the Tab are restyled as links
2120
     * - major actions can provide alternate states for richer presentation (see ssui.button widget extension)
2121
     *
2122
     * @return FieldList The available actions for this page.
2123
     */
2124
    public function getCMSActions()
2125
    {
2126
        // Get status of page
2127
        $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...
2128
        $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...
2129
        $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...
2130
2131
        // Check permissions
2132
        $canPublish = $this->canPublish();
2133
        $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...
2134
        $canEdit = $this->canEdit();
2135
2136
        // Major actions appear as buttons immediately visible as page actions.
2137
        $majorActions = CompositeField::create()->setName('MajorActions');
2138
        $majorActions->setFieldHolderTemplate(get_class($majorActions) . '_holder_buttongroup');
2139
2140
        // Minor options are hidden behind a drop-up and appear as links (although they are still FormActions).
2141
        $rootTabSet = new TabSet('ActionMenus');
2142
        $moreOptions = new Tab(
2143
            'MoreOptions',
2144
            _t(__CLASS__.'.MoreOptions', 'More options', 'Expands a view for more buttons')
2145
        );
2146
        $moreOptions->addExtraClass('popover-actions-simulate');
2147
        $rootTabSet->push($moreOptions);
2148
        $rootTabSet->addExtraClass('ss-ui-action-tabset action-menus noborder');
2149
2150
        // Render page information into the "more-options" drop-up, on the top.
2151
        $liveRecord = Versioned::get_by_stage(self::class, Versioned::LIVE)->byID($this->ID);
2152
        $infoTemplate = SSViewer::get_templates_by_class(static::class, '_Information', self::class);
2153
        $moreOptions->push(
2154
            new LiteralField(
2155
                'Information',
2156
                $this->customise(array(
2157
                    'Live' => $liveRecord,
2158
                    'ExistsOnLive' => $isPublished
2159
                ))->renderWith($infoTemplate)
2160
            )
2161
        );
2162
2163
        // Add to campaign option if not-archived and has publish permission
2164
        if (($isPublished || $isOnDraft) && $canPublish) {
2165
            $moreOptions->push(
2166
                AddToCampaignHandler_FormAction::create()
2167
                    ->removeExtraClass('btn-primary')
2168
                    ->addExtraClass('btn-secondary')
2169
            );
2170
        }
2171
2172
        // "readonly"/viewing version that isn't the current version of the record
2173
        $stageRecord = Versioned::get_by_stage(static::class, Versioned::DRAFT)->byID($this->ID);
2174
        /** @skipUpgrade */
2175
        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...
2176
            $moreOptions->push(FormAction::create('email', _t('SilverStripe\\CMS\\Controllers\\CMSMain.EMAIL', 'Email')));
2177
            $moreOptions->push(FormAction::create('rollback', _t('SilverStripe\\CMS\\Controllers\\CMSMain.ROLLBACK', 'Roll back to this version')));
2178
            $actions = new FieldList(array($majorActions, $rootTabSet));
2179
2180
            // getCMSActions() can be extended with updateCMSActions() on a extension
2181
            $this->extend('updateCMSActions', $actions);
2182
            return $actions;
2183
        }
2184
2185
        // "unpublish"
2186 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...
2187
            $moreOptions->push(
2188
                FormAction::create('unpublish', _t(__CLASS__.'.BUTTONUNPUBLISH', 'Unpublish'), 'delete')
2189
                    ->setDescription(_t(__CLASS__.'.BUTTONUNPUBLISHDESC', 'Remove this page from the published site'))
2190
                    ->addExtraClass('btn-secondary')
2191
            );
2192
        }
2193
2194
        // "rollback"
2195 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...
2196
            $moreOptions->push(
2197
                FormAction::create('rollback', _t(__CLASS__.'.BUTTONCANCELDRAFT', 'Cancel draft changes'))
2198
                    ->setDescription(_t(
2199
                        'SilverStripe\\CMS\\Model\\SiteTree.BUTTONCANCELDRAFTDESC',
2200
                        'Delete your draft and revert to the currently published page'
2201
                    ))
2202
                    ->addExtraClass('btn-secondary')
2203
            );
2204
        }
2205
2206
        // "restore"
2207
        if ($canEdit && !$isOnDraft && $isPublished) {
2208
            $majorActions->push(FormAction::create('revert', _t('SilverStripe\\CMS\\Controllers\\CMSMain.RESTORE', 'Restore')));
2209
        }
2210
2211
        // Check if we can restore a deleted page
2212
        // Note: It would be nice to have a canRestore() permission at some point
2213
        if ($canEdit && !$isOnDraft && !$isPublished) {
2214
            // Determine if we should force a restore to root (where once it was a subpage)
2215
            $restoreToRoot = $this->isParentArchived();
2216
2217
            // "restore"
2218
            $title = $restoreToRoot
2219
                ? _t('SilverStripe\\CMS\\Controllers\\CMSMain.RESTORE_TO_ROOT', 'Restore draft at top level')
2220
                : _t('SilverStripe\\CMS\\Controllers\\CMSMain.RESTORE', 'Restore draft');
2221
            $description = $restoreToRoot
2222
                ? _t('SilverStripe\\CMS\\Controllers\\CMSMain.RESTORE_TO_ROOT_DESC', 'Restore the archived version to draft as a top level page')
2223
                : _t('SilverStripe\\CMS\\Controllers\\CMSMain.RESTORE_DESC', 'Restore the archived version to draft');
2224
            $majorActions->push(
2225
                FormAction::create('restore', $title)
2226
                    ->setDescription($description)
2227
                    ->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...
2228
                    ->setAttribute('data-icon', 'decline')
2229
            );
2230
        }
2231
2232
        // If a page is on any stage it can be archived
2233
        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...
2234
            $title = $isPublished
2235
                ? _t('SilverStripe\\CMS\\Controllers\\CMSMain.UNPUBLISH_AND_ARCHIVE', 'Unpublish and archive')
2236
                : _t('SilverStripe\\CMS\\Controllers\\CMSMain.ARCHIVE', 'Archive');
2237
            $moreOptions->push(
2238
                FormAction::create('archive', $title)
2239
                    ->addExtraClass('delete btn btn-secondary')
2240
                    ->setDescription(_t(
2241
                        'SilverStripe\\CMS\\Model\\SiteTree.BUTTONDELETEDESC',
2242
                        'Remove from draft/live and send to archive'
2243
                    ))
2244
            );
2245
        }
2246
2247
        // "save", supports an alternate state that is still clickable, but notifies the user that the action is not needed.
2248
        if ($canEdit && $isOnDraft) {
2249
            $majorActions->push(
2250
                FormAction::create('save', _t(__CLASS__.'.BUTTONSAVED', 'Saved'))
2251
                    ->addExtraClass('btn-secondary-outline font-icon-check-mark')
2252
                    ->setAttribute('data-btn-alternate', 'btn action btn-primary font-icon-save')
2253
                    ->setUseButtonTag(true)
2254
                    ->setAttribute('data-text-alternate', _t('SilverStripe\\CMS\\Controllers\\CMSMain.SAVEDRAFT', 'Save draft'))
2255
            );
2256
        }
2257
2258
        if ($canPublish && $isOnDraft) {
2259
            // "publish", as with "save", it supports an alternate state to show when action is needed.
2260
            $majorActions->push(
2261
                $publish = FormAction::create('publish', _t(__CLASS__.'.BUTTONPUBLISHED', 'Published'))
2262
                    ->addExtraClass('btn-secondary-outline font-icon-check-mark')
2263
                    ->setAttribute('data-btn-alternate', 'btn action btn-primary font-icon-rocket')
2264
                    ->setUseButtonTag(true)
2265
                    ->setAttribute('data-text-alternate', _t(__CLASS__.'.BUTTONSAVEPUBLISH', 'Save & publish'))
2266
            );
2267
2268
            // Set up the initial state of the button to reflect the state of the underlying SiteTree object.
2269
            if ($stagesDiffer) {
2270
                $publish->addExtraClass('btn-primary font-icon-rocket');
2271
                $publish->setTitle(_t(__CLASS__.'.BUTTONSAVEPUBLISH', 'Save & publish'));
2272
                $publish->removeExtraClass('btn-secondary-outline font-icon-check-mark');
2273
            }
2274
        }
2275
2276
        $actions = new FieldList(array($majorActions, $rootTabSet));
2277
2278
        // Hook for extensions to add/remove actions.
2279
        $this->extend('updateCMSActions', $actions);
2280
2281
        return $actions;
2282
    }
2283
2284
    public function onAfterPublish()
2285
    {
2286
        // Force live sort order to match stage sort order
2287
        DB::prepared_query(
2288
            'UPDATE "SiteTree_Live"
2289
			SET "Sort" = (SELECT "SiteTree"."Sort" FROM "SiteTree" WHERE "SiteTree_Live"."ID" = "SiteTree"."ID")
2290
			WHERE EXISTS (SELECT "SiteTree"."Sort" FROM "SiteTree" WHERE "SiteTree_Live"."ID" = "SiteTree"."ID") AND "ParentID" = ?',
2291
            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...
2292
        );
2293
    }
2294
2295
    /**
2296
     * Update draft dependant pages
2297
     */
2298
    public function onAfterRevertToLive()
2299
    {
2300
        // Use an alias to get the updates made by $this->publish
2301
        /** @var SiteTree $stageSelf */
2302
        $stageSelf = Versioned::get_by_stage(self::class, Versioned::DRAFT)->byID($this->ID);
2303
        $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...
2304
2305
        // Need to update pages linking to this one as no longer broken
2306
        foreach ($stageSelf->DependentPages() as $page) {
2307
            /** @var SiteTree $page */
2308
            $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...
2309
        }
2310
    }
2311
2312
    /**
2313
     * Determine if this page references a parent which is archived, and not available in stage
2314
     *
2315
     * @return bool True if there is an archived parent
2316
     */
2317
    protected function isParentArchived()
2318
    {
2319
        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...
2320
            /** @var SiteTree $parentPage */
2321
            $parentPage = Versioned::get_latest_version(self::class, $parentID);
2322
            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...
2323
                return true;
2324
            }
2325
        }
2326
        return false;
2327
    }
2328
2329
    /**
2330
     * Restore the content in the active copy of this SiteTree page to the stage site.
2331
     *
2332
     * @return self
2333
     */
2334
    public function doRestoreToStage()
2335
    {
2336
        $this->invokeWithExtensions('onBeforeRestoreToStage', $this);
2337
2338
        // Ensure that the parent page is restored, otherwise restore to root
2339
        if ($this->isParentArchived()) {
2340
            $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...
2341
        }
2342
2343
        // if no record can be found on draft stage (meaning it has been "deleted from draft" before),
2344
        // create an empty record
2345
        if (!DB::prepared_query("SELECT \"ID\" FROM \"SiteTree\" WHERE \"ID\" = ?", array($this->ID))->value()) {
2346
            $conn = DB::get_conn();
2347
            if (method_exists($conn, 'allowPrimaryKeyEditing')) {
2348
                $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...
2349
            }
2350
            DB::prepared_query("INSERT INTO \"SiteTree\" (\"ID\") VALUES (?)", array($this->ID));
2351
            if (method_exists($conn, 'allowPrimaryKeyEditing')) {
2352
                $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...
2353
            }
2354
        }
2355
2356
        $oldReadingMode = Versioned::get_reading_mode();
2357
        Versioned::set_stage(Versioned::DRAFT);
2358
        $this->forceChange();
2359
        $this->write();
2360
2361
        /** @var SiteTree $result */
2362
        $result = DataObject::get_by_id(self::class, $this->ID);
2363
2364
        Versioned::set_reading_mode($oldReadingMode);
2365
2366
        // Need to update pages linking to this one as no longer broken
2367
        $this->updateDependentPages();
2368
2369
        $this->invokeWithExtensions('onAfterRestoreToStage', $this);
2370
2371
        return $result;
2372
    }
2373
2374
    /**
2375
     * Check if this page is new - that is, if it has yet to have been written to the database.
2376
     *
2377
     * @return bool
2378
     */
2379
    public function isNew()
2380
    {
2381
        /**
2382
         * This check was a problem for a self-hosted site, and may indicate a bug in the interpreter on their server,
2383
         * or a bug here. Changing the condition from empty($this->ID) to !$this->ID && !$this->record['ID'] fixed this.
2384
         */
2385
        if (empty($this->ID)) {
2386
            return true;
2387
        }
2388
2389
        if (is_numeric($this->ID)) {
2390
            return false;
2391
        }
2392
2393
        return stripos($this->ID, 'new') === 0;
2394
    }
2395
2396
    /**
2397
     * Get the class dropdown used in the CMS to change the class of a page. This returns the list of options in the
2398
     * dropdown as a Map from class name to singular name. Filters by {@link SiteTree->canCreate()}, as well as
2399
     * {@link SiteTree::$needs_permission}.
2400
     *
2401
     * @return array
2402
     */
2403
    protected function getClassDropdown()
2404
    {
2405
        $classes = self::page_type_classes();
2406
        $currentClass = null;
2407
2408
        $result = array();
2409
        foreach ($classes as $class) {
2410
            $instance = singleton($class);
2411
2412
            // if the current page type is this the same as the class type always show the page type in the list
2413
            if ($this->ClassName != $instance->ClassName) {
2414
                if ($instance instanceof HiddenClass) {
2415
                    continue;
2416
                }
2417
                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...
2418
                    continue;
2419
                }
2420
            }
2421
2422
            if ($perms = $instance->stat('need_permission')) {
2423
                if (!$this->can($perms)) {
2424
                    continue;
2425
                }
2426
            }
2427
2428
            $pageTypeName = $instance->i18n_singular_name();
2429
2430
            $currentClass = $class;
2431
            $result[$class] = $pageTypeName;
2432
2433
            // If we're in translation mode, the link between the translated pagetype title and the actual classname
2434
            // might not be obvious, so we add it in parantheses. Example: class "RedirectorPage" has the title
2435
            // "Weiterleitung" in German, so it shows up as "Weiterleitung (RedirectorPage)"
2436
            if (i18n::getData()->langFromLocale(i18n::get_locale()) != 'en') {
2437
                $result[$class] = $result[$class] .  " ({$class})";
2438
            }
2439
        }
2440
2441
        // sort alphabetically, and put current on top
2442
        asort($result);
2443
        if ($currentClass) {
2444
            $currentPageTypeName = $result[$currentClass];
2445
            unset($result[$currentClass]);
2446
            $result = array_reverse($result);
2447
            $result[$currentClass] = $currentPageTypeName;
2448
            $result = array_reverse($result);
2449
        }
2450
2451
        return $result;
2452
    }
2453
2454
    /**
2455
     * Returns an array of the class names of classes that are allowed to be children of this class.
2456
     *
2457
     * @return string[]
2458
     */
2459
    public function allowedChildren()
2460
    {
2461
        // Get config based on old FIRST_SET rules
2462
        $candidates = null;
2463
        $class = get_class($this);
2464
        while ($class) {
2465
            if (Config::inst()->exists($class, 'allowed_children', Config::UNINHERITED)) {
2466
                $candidates = Config::inst()->get($class, 'allowed_children', Config::UNINHERITED);
2467
                break;
2468
            }
2469
            $class = get_parent_class($class);
2470
        }
2471
        if (!$candidates || $candidates === 'none' || $candidates === 'SiteTree_root') {
2472
            return [];
2473
        }
2474
2475
        // Parse candidate list
2476
        $allowedChildren = [];
2477
        foreach ($candidates as $candidate) {
2478
            // If a classname is prefixed by "*", such as "*Page", then only that class is allowed - no subclasses.
2479
            // Otherwise, the class and all its subclasses are allowed.
2480
            if (substr($candidate, 0, 1) == '*') {
2481
                $allowedChildren[] = substr($candidate, 1);
2482
            } elseif ($subclasses = ClassInfo::subclassesFor($candidate)) {
2483
                foreach ($subclasses as $subclass) {
2484
                    if ($subclass == 'SiteTree_root' || singleton($subclass) instanceof HiddenClass) {
2485
                        continue;
2486
                    }
2487
                    $allowedChildren[] = $subclass;
2488
                }
2489
            }
2490
        }
2491
        return $allowedChildren;
2492
    }
2493
2494
    /**
2495
     * Returns the class name of the default class for children of this page.
2496
     *
2497
     * @return string
2498
     */
2499
    public function defaultChild()
2500
    {
2501
        $default = $this->stat('default_child');
2502
        $allowed = $this->allowedChildren();
2503
        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...
2504
            if (!$default || !in_array($default, $allowed)) {
2505
                $default = reset($allowed);
2506
            }
2507
            return $default;
2508
        }
2509
        return null;
2510
    }
2511
2512
    /**
2513
     * Returns the class name of the default class for the parent of this page.
2514
     *
2515
     * @return string
2516
     */
2517
    public function defaultParent()
2518
    {
2519
        return $this->stat('default_parent');
2520
    }
2521
2522
    /**
2523
     * Get the title for use in menus for this page. If the MenuTitle field is set it returns that, else it returns the
2524
     * Title field.
2525
     *
2526
     * @return string
2527
     */
2528
    public function getMenuTitle()
2529
    {
2530
        if ($value = $this->getField("MenuTitle")) {
2531
            return $value;
2532
        } else {
2533
            return $this->getField("Title");
2534
        }
2535
    }
2536
2537
2538
    /**
2539
     * Set the menu title for this page.
2540
     *
2541
     * @param string $value
2542
     */
2543
    public function setMenuTitle($value)
2544
    {
2545
        if ($value == $this->getField("Title")) {
2546
            $this->setField("MenuTitle", null);
2547
        } else {
2548
            $this->setField("MenuTitle", $value);
2549
        }
2550
    }
2551
2552
    /**
2553
     * A flag provides the user with additional data about the current page status, for example a "removed from draft"
2554
     * status. Each page can have more than one status flag. Returns a map of a unique key to a (localized) title for
2555
     * the flag. The unique key can be reused as a CSS class. Use the 'updateStatusFlags' extension point to customize
2556
     * the flags.
2557
     *
2558
     * Example (simple):
2559
     *   "deletedonlive" => "Deleted"
2560
     *
2561
     * Example (with optional title attribute):
2562
     *   "deletedonlive" => array('text' => "Deleted", 'title' => 'This page has been deleted')
2563
     *
2564
     * @param bool $cached Whether to serve the fields from cache; false regenerate them
2565
     * @return array
2566
     */
2567
    public function getStatusFlags($cached = true)
2568
    {
2569
        if (!$this->_cache_statusFlags || !$cached) {
2570
            $flags = array();
2571
            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...
2572
                $flags['removedfromdraft'] = array(
2573
                    'text' => _t(__CLASS__.'.ONLIVEONLYSHORT', 'On live only'),
2574
                    'title' => _t(__CLASS__.'.ONLIVEONLYSHORTHELP', 'Page is published, but has been deleted from draft'),
2575
                );
2576
            } 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...
2577
                $flags['archived'] = array(
2578
                    'text' => _t(__CLASS__.'.ARCHIVEDPAGESHORT', 'Archived'),
2579
                    'title' => _t(__CLASS__.'.ARCHIVEDPAGEHELP', 'Page is removed from draft and live'),
2580
                );
2581
            } 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...
2582
                $flags['addedtodraft'] = array(
2583
                    'text' => _t(__CLASS__.'.ADDEDTODRAFTSHORT', 'Draft'),
2584
                    'title' => _t(__CLASS__.'.ADDEDTODRAFTHELP', "Page has not been published yet")
2585
                );
2586
            } 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...
2587
                $flags['modified'] = array(
2588
                    'text' => _t(__CLASS__.'.MODIFIEDONDRAFTSHORT', 'Modified'),
2589
                    'title' => _t(__CLASS__.'.MODIFIEDONDRAFTHELP', 'Page has unpublished changes'),
2590
                );
2591
            }
2592
2593
            $this->extend('updateStatusFlags', $flags);
2594
2595
            $this->_cache_statusFlags = $flags;
2596
        }
2597
2598
        return $this->_cache_statusFlags;
2599
    }
2600
2601
    /**
2602
     * getTreeTitle will return three <span> html DOM elements, an empty <span> with the class 'jstree-pageicon' in
2603
     * front, following by a <span> wrapping around its MenutTitle, then following by a <span> indicating its
2604
     * publication status.
2605
     *
2606
     * @return string An HTML string ready to be directly used in a template
2607
     */
2608
    public function getTreeTitle()
2609
    {
2610
        // Build the list of candidate children
2611
        $children = array();
2612
        $candidates = static::page_type_classes();
2613
        foreach ($this->allowedChildren() as $childClass) {
2614
            if (!in_array($childClass, $candidates)) {
2615
                continue;
2616
            }
2617
            $child = singleton($childClass);
2618
            if ($child->canCreate(null, array('Parent' => $this))) {
2619
                $children[$childClass] = $child->i18n_singular_name();
2620
            }
2621
        }
2622
        $flags = $this->getStatusFlags();
2623
        $treeTitle = sprintf(
2624
            "<span class=\"jstree-pageicon\"></span><span class=\"item\" data-allowedchildren=\"%s\">%s</span>",
2625
            Convert::raw2att(Convert::raw2json($children)),
2626
            Convert::raw2xml(str_replace(array("\n","\r"), "", $this->MenuTitle))
2627
        );
2628
        foreach ($flags as $class => $data) {
2629
            if (is_string($data)) {
2630
                $data = array('text' => $data);
2631
            }
2632
            $treeTitle .= sprintf(
2633
                "<span class=\"badge %s\"%s>%s</span>",
2634
                'status-' . Convert::raw2xml($class),
2635
                (isset($data['title'])) ? sprintf(' title="%s"', Convert::raw2xml($data['title'])) : '',
2636
                Convert::raw2xml($data['text'])
2637
            );
2638
        }
2639
2640
        return $treeTitle;
2641
    }
2642
2643
    /**
2644
     * Returns the page in the current page stack of the given level. Level(1) will return the main menu item that
2645
     * we're currently inside, etc.
2646
     *
2647
     * @param int $level
2648
     * @return SiteTree
2649
     */
2650
    public function Level($level)
2651
    {
2652
        $parent = $this;
2653
        $stack = array($parent);
2654
        while (($parent = $parent->Parent()) && $parent->exists()) {
2655
            array_unshift($stack, $parent);
2656
        }
2657
2658
        return isset($stack[$level-1]) ? $stack[$level-1] : null;
2659
    }
2660
2661
    /**
2662
     * Gets the depth of this page in the sitetree, where 1 is the root level
2663
     *
2664
     * @return int
2665
     */
2666
    public function getPageLevel()
2667
    {
2668
        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...
2669
            return 1 + $this->Parent()->getPageLevel();
2670
        }
2671
        return 1;
2672
    }
2673
2674
    /**
2675
     * Find the controller name by our convention of {$ModelClass}Controller
2676
     *
2677
     * @return string
2678
     */
2679
    public function getControllerName()
2680
    {
2681
        //default controller for SiteTree objects
2682
        $controller = ContentController::class;
2683
2684
        //go through the ancestry for this class looking for
2685
        $ancestry = ClassInfo::ancestry(static::class);
2686
        // loop over the array going from the deepest descendant (ie: the current class) to SiteTree
2687
        while ($class = array_pop($ancestry)) {
2688
            //we don't need to go any deeper than the SiteTree class
2689
            if ($class == SiteTree::class) {
2690
                break;
2691
            }
2692
            // If we have a class of "{$ClassName}Controller" then we found our controller
2693
            if (class_exists($candidate = sprintf('%sController', $class))) {
2694
                $controller = $candidate;
2695
                break;
2696
            } elseif (class_exists($candidate = sprintf('%s_Controller', $class))) {
2697
                // Support the legacy underscored filename, but raise a deprecation notice
2698
                Deprecation::notice(
2699
                    '5.0',
2700
                    'Underscored controller class names are deprecated. Use "MyController" instead of "My_Controller".',
2701
                    Deprecation::SCOPE_GLOBAL
2702
                );
2703
                $controller = $candidate;
2704
                break;
2705
            }
2706
        }
2707
2708
        return $controller;
2709
    }
2710
2711
    /**
2712
     * Return the CSS classes to apply to this node in the CMS tree.
2713
     *
2714
     * @return string
2715
     */
2716
    public function CMSTreeClasses()
2717
    {
2718
        $classes = sprintf('class-%s', static::class);
2719
        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...
2720
            $classes .= " BrokenLink";
2721
        }
2722
2723
        if (!$this->canAddChildren()) {
2724
            $classes .= " nochildren";
2725
        }
2726
2727
        if (!$this->canEdit() && !$this->canAddChildren()) {
2728
            if (!$this->canView()) {
2729
                $classes .= " disabled";
2730
            } else {
2731
                $classes .= " edit-disabled";
2732
            }
2733
        }
2734
2735
        if (!$this->ShowInMenus) {
2736
            $classes .= " notinmenu";
2737
        }
2738
2739
        return $classes;
2740
    }
2741
2742
    /**
2743
     * Stops extendCMSFields() being called on getCMSFields(). This is useful when you need access to fields added by
2744
     * subclasses of SiteTree in a extension. Call before calling parent::getCMSFields(), and reenable afterwards.
2745
     */
2746
    public static function disableCMSFieldsExtensions()
2747
    {
2748
        self::$runCMSFieldsExtensions = false;
2749
    }
2750
2751
    /**
2752
     * Reenables extendCMSFields() being called on getCMSFields() after it has been disabled by
2753
     * disableCMSFieldsExtensions().
2754
     */
2755
    public static function enableCMSFieldsExtensions()
2756
    {
2757
        self::$runCMSFieldsExtensions = true;
2758
    }
2759
2760
    public function providePermissions()
2761
    {
2762
        return array(
2763
            'SITETREE_GRANT_ACCESS' => array(
2764
                'name' => _t(__CLASS__.'.PERMISSION_GRANTACCESS_DESCRIPTION', 'Manage access rights for content'),
2765
                'help' => _t(__CLASS__.'.PERMISSION_GRANTACCESS_HELP', 'Allow setting of page-specific access restrictions in the "Pages" section.'),
2766
                'category' => _t('SilverStripe\\Security\\Permission.PERMISSIONS_CATEGORY', 'Roles and access permissions'),
2767
                'sort' => 100
2768
            ),
2769
            'SITETREE_VIEW_ALL' => array(
2770
                'name' => _t(__CLASS__.'.VIEW_ALL_DESCRIPTION', 'View any page'),
2771
                'category' => _t('SilverStripe\\Security\\Permission.CONTENT_CATEGORY', 'Content permissions'),
2772
                'sort' => -100,
2773
                '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')
2774
            ),
2775
            'SITETREE_EDIT_ALL' => array(
2776
                'name' => _t(__CLASS__.'.EDIT_ALL_DESCRIPTION', 'Edit any page'),
2777
                'category' => _t('SilverStripe\\Security\\Permission.CONTENT_CATEGORY', 'Content permissions'),
2778
                'sort' => -50,
2779
                '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')
2780
            ),
2781
            'SITETREE_REORGANISE' => array(
2782
                'name' => _t(__CLASS__.'.REORGANISE_DESCRIPTION', 'Change site structure'),
2783
                'category' => _t('SilverStripe\\Security\\Permission.CONTENT_CATEGORY', 'Content permissions'),
2784
                'help' => _t(__CLASS__.'.REORGANISE_HELP', 'Rearrange pages in the site tree through drag&drop.'),
2785
                'sort' => 100
2786
            ),
2787
            'VIEW_DRAFT_CONTENT' => array(
2788
                'name' => _t(__CLASS__.'.VIEW_DRAFT_CONTENT', 'View draft content'),
2789
                'category' => _t('SilverStripe\\Security\\Permission.CONTENT_CATEGORY', 'Content permissions'),
2790
                '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.'),
2791
                'sort' => 100
2792
            )
2793
        );
2794
    }
2795
2796
    /**
2797
     * Default singular name for page / sitetree
2798
     *
2799
     * @return string
2800
     */
2801 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...
2802
    {
2803
        $base = in_array(static::class, [Page::class, self::class]);
2804
        if ($base) {
2805
            return $this->stat('base_singular_name');
2806
        }
2807
        return parent::singular_name();
2808
    }
2809
2810
    /**
2811
     * Default plural name for page / sitetree
2812
     *
2813
     * @return string
2814
     */
2815 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...
2816
    {
2817
        $base = in_array(static::class, [Page::class, self::class]);
2818
        if ($base) {
2819
            return $this->stat('base_plural_name');
2820
        }
2821
        return parent::plural_name();
2822
    }
2823
2824
    /**
2825
     * Get description for this page type
2826
     *
2827
     * @return string|null
2828
     */
2829
    public function classDescription()
2830
    {
2831
        $base = in_array(static::class, [Page::class, self::class]);
2832
        if ($base) {
2833
            return $this->stat('base_description');
2834
        }
2835
        return $this->stat('description');
2836
    }
2837
2838
    /**
2839
     * Get localised description for this page
2840
     *
2841
     * @return string|null
2842
     */
2843
    public function i18n_classDescription()
2844
    {
2845
        $description = $this->classDescription();
2846
        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...
2847
            return _t(static::class.'.DESCRIPTION', $description);
2848
        }
2849
        return null;
2850
    }
2851
2852
    /**
2853
     * Overloaded to also provide entities for 'Page' class which is usually located in custom code, hence textcollector
2854
     * picks it up for the wrong folder.
2855
     *
2856
     * @return array
2857
     */
2858
    public function provideI18nEntities()
2859
    {
2860
        $entities = parent::provideI18nEntities();
2861
2862
        // Add optional description
2863
        $description = $this->classDescription();
2864
        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...
2865
            $entities[static::class . '.DESCRIPTION'] = $description;
2866
        }
2867
        return $entities;
2868
    }
2869
2870
    /**
2871
     * Returns 'root' if the current page has no parent, or 'subpage' otherwise
2872
     *
2873
     * @return string
2874
     */
2875
    public function getParentType()
2876
    {
2877
        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...
2878
    }
2879
2880
    /**
2881
     * Clear the permissions cache for SiteTree
2882
     */
2883
    public static function reset()
2884
    {
2885
        $permissions = static::getPermissionChecker();
2886
        if ($permissions instanceof InheritedPermissions) {
2887
            $permissions->clearCache();
2888
        }
2889
    }
2890
2891
    /**
2892
     * Update dependant pages
2893
     */
2894
    protected function updateDependentPages()
2895
    {
2896
        // Need to flush cache to avoid outdated versionnumber references
2897
        $this->flushCache();
2898
2899
        // Need to mark pages depending to this one as broken
2900
        $dependentPages = $this->DependentPages();
2901
        if ($dependentPages) {
2902
            foreach ($dependentPages as $page) {
2903
                // $page->write() calls syncLinkTracking, which does all the hard work for us.
2904
                $page->write();
2905
            }
2906
        }
2907
    }
2908
}
2909