Completed
Pull Request — master (#1811)
by Damian
02:43
created

SiteTree::canEdit()   A

Complexity

Conditions 4
Paths 6

Size

Total Lines 21
Code Lines 10

Duplication

Lines 21
Ratio 100 %

Importance

Changes 0
Metric Value
dl 21
loc 21
rs 9.0534
c 0
b 0
f 0
cc 4
eloc 10
nc 6
nop 1
1
<?php
2
3
namespace SilverStripe\CMS\Model;
4
5
use Page;
6
use SilverStripe\CampaignAdmin\AddToCampaignHandler_FormAction;
7
use SilverStripe\Core\Injector\Injector;
8
use SilverStripe\ORM\CMSPreviewable;
9
use SilverStripe\CMS\Controllers\CMSPageEditController;
10
use SilverStripe\CMS\Controllers\ContentController;
11
use SilverStripe\CMS\Controllers\ModelAsController;
12
use SilverStripe\CMS\Controllers\RootURLController;
13
use SilverStripe\CMS\Forms\SiteTreeURLSegmentField;
14
use SilverStripe\Control\ContentNegotiator;
15
use SilverStripe\Control\Controller;
16
use SilverStripe\Control\Director;
17
use SilverStripe\Control\RequestHandler;
18
use SilverStripe\Core\ClassInfo;
19
use SilverStripe\Core\Config\Config;
20
use SilverStripe\Core\Convert;
21
use SilverStripe\Core\Resettable;
22
use SilverStripe\Dev\Deprecation;
23
use SilverStripe\Forms\CheckboxField;
24
use SilverStripe\Forms\CompositeField;
25
use SilverStripe\Forms\DropdownField;
26
use SilverStripe\Forms\FieldGroup;
27
use SilverStripe\Forms\FieldList;
28
use SilverStripe\Forms\FormAction;
29
use SilverStripe\Forms\FormField;
30
use SilverStripe\Forms\GridField\GridField;
31
use SilverStripe\Forms\GridField\GridFieldDataColumns;
32
use SilverStripe\Forms\HTMLEditor\HTMLEditorField;
33
use SilverStripe\Forms\ListboxField;
34
use SilverStripe\Forms\LiteralField;
35
use SilverStripe\Forms\OptionsetField;
36
use SilverStripe\Forms\Tab;
37
use SilverStripe\Forms\TabSet;
38
use SilverStripe\Forms\TextareaField;
39
use SilverStripe\Forms\TextField;
40
use SilverStripe\Forms\ToggleCompositeField;
41
use SilverStripe\Forms\TreeDropdownField;
42
use SilverStripe\i18n\i18n;
43
use SilverStripe\i18n\i18nEntityProvider;
44
use SilverStripe\ORM\ArrayList;
45
use SilverStripe\ORM\DataList;
46
use SilverStripe\ORM\DataObject;
47
use SilverStripe\ORM\DB;
48
use SilverStripe\ORM\HiddenClass;
49
use SilverStripe\ORM\Hierarchy\Hierarchy;
50
use SilverStripe\ORM\ManyManyList;
51
use SilverStripe\ORM\ValidationResult;
52
use SilverStripe\Security\InheritedPermissions;
53
use SilverStripe\Security\InheritedPermissionsExtension;
54
use SilverStripe\Security\PermissionChecker;
55
use SilverStripe\Versioned\Versioned;
56
use SilverStripe\Security\Group;
57
use SilverStripe\Security\Member;
58
use SilverStripe\Security\Permission;
59
use SilverStripe\Security\PermissionProvider;
60
use SilverStripe\SiteConfig\SiteConfig;
61
use SilverStripe\View\ArrayData;
62
use SilverStripe\View\Parsers\ShortcodeParser;
63
use SilverStripe\View\Parsers\URLSegmentFilter;
64
use SilverStripe\View\SSViewer;
65
use Subsite;
66
67
/**
68
 * Basic data-object representing all pages within the site tree. All page types that live within the hierarchy should
69
 * inherit from this. In addition, it contains a number of static methods for querying the site tree and working with
70
 * draft and published states.
71
 *
72
 * <h2>URLs</h2>
73
 * A page is identified during request handling via its "URLSegment" database column. As pages can be nested, the full
74
 * path of a URL might contain multiple segments. Each segment is stored in its filtered representation (through
75
 * {@link URLSegmentFilter}). The full path is constructed via {@link Link()}, {@link RelativeLink()} and
76
 * {@link AbsoluteLink()}. You can allow these segments to contain multibyte characters through
77
 * {@link URLSegmentFilter::$default_allow_multibyte}.
78
 *
79
 * @property string URLSegment
80
 * @property string Title
81
 * @property string MenuTitle
82
 * @property string Content HTML content of the page.
83
 * @property string MetaDescription
84
 * @property string ExtraMeta
85
 * @property string ShowInMenus
86
 * @property string ShowInSearch
87
 * @property string Sort Integer value denoting the sort order.
88
 * @property string ReportClass
89
 *
90
 * @method ManyManyList ViewerGroups() List of groups that can view this object.
91
 * @method ManyManyList EditorGroups() List of groups that can edit this object.
92
 * @method SiteTree Parent()
93
 *
94
 * @mixin Hierarchy
95
 * @mixin Versioned
96
 * @mixin SiteTreeLinkTracking
97
 * @mixin InheritedPermissionsExtension
98
 */
99
class SiteTree extends DataObject implements PermissionProvider, i18nEntityProvider, CMSPreviewable, Resettable
100
{
101
102
    /**
103
     * Indicates what kind of children this page type can have.
104
     * This can be an array of allowed child classes, or the string "none" -
105
     * indicating that this page type can't have children.
106
     * If a classname is prefixed by "*", such as "*Page", then only that
107
     * class is allowed - no subclasses. Otherwise, the class and all its
108
     * subclasses are allowed.
109
     * To control allowed children on root level (no parent), use {@link $can_be_root}.
110
     *
111
     * Note that this setting is cached when used in the CMS, use the "flush" query parameter to clear it.
112
     *
113
     * @config
114
     * @var array
115
     */
116
    private static $allowed_children = [
117
        self::class
118
    ];
119
120
    /**
121
     * The default child class for this page.
122
     * Note: Value might be cached, see {@link $allowed_chilren}.
123
     *
124
     * @config
125
     * @var string
126
     */
127
    private static $default_child = "Page";
128
129
    /**
130
     * Default value for SiteTree.ClassName enum
131
     * {@see DBClassName::getDefault}
132
     *
133
     * @config
134
     * @var string
135
     */
136
    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...
137
138
    /**
139
     * The default parent class for this page.
140
     * Note: Value might be cached, see {@link $allowed_chilren}.
141
     *
142
     * @config
143
     * @var string
144
     */
145
    private static $default_parent = null;
146
147
    /**
148
     * Controls whether a page can be in the root of the site tree.
149
     * Note: Value might be cached, see {@link $allowed_chilren}.
150
     *
151
     * @config
152
     * @var bool
153
     */
154
    private static $can_be_root = true;
155
156
    /**
157
     * List of permission codes a user can have to allow a user to create a page of this type.
158
     * Note: Value might be cached, see {@link $allowed_chilren}.
159
     *
160
     * @config
161
     * @var array
162
     */
163
    private static $need_permission = null;
164
165
    /**
166
     * If you extend a class, and don't want to be able to select the old class
167
     * in the cms, set this to the old class name. Eg, if you extended Product
168
     * to make ImprovedProduct, then you would set $hide_ancestor to Product.
169
     *
170
     * @config
171
     * @var string
172
     */
173
    private static $hide_ancestor = null;
174
175
    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...
176
        "URLSegment" => "Varchar(255)",
177
        "Title" => "Varchar(255)",
178
        "MenuTitle" => "Varchar(100)",
179
        "Content" => "HTMLText",
180
        "MetaDescription" => "Text",
181
        "ExtraMeta" => "HTMLFragment(['whitelist' => ['meta', 'link']])",
182
        "ShowInMenus" => "Boolean",
183
        "ShowInSearch" => "Boolean",
184
        "Sort" => "Int",
185
        "HasBrokenFile" => "Boolean",
186
        "HasBrokenLink" => "Boolean",
187
        "ReportClass" => "Varchar",
188
    );
189
190
    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...
191
        "URLSegment" => true,
192
    );
193
194
    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...
195
        "VirtualPages" => "SilverStripe\\CMS\\Model\\VirtualPage.CopyContentFrom"
196
    );
197
198
    private static $owned_by = array(
199
        "VirtualPages"
200
    );
201
202
    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...
203
        "Breadcrumbs" => "HTMLFragment",
204
        "LastEdited" => "Datetime",
205
        "Created" => "Datetime",
206
        'Link' => 'Text',
207
        'RelativeLink' => 'Text',
208
        'AbsoluteLink' => 'Text',
209
        'CMSEditLink' => 'Text',
210
        'TreeTitle' => 'HTMLFragment',
211
        'MetaTags' => 'HTMLFragment',
212
    );
213
214
    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...
215
        "ShowInMenus" => 1,
216
        "ShowInSearch" => 1,
217
    );
218
219
    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...
220
221
    private static $versioning = array(
222
        "Stage",  "Live"
223
    );
224
225
    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...
226
227
    /**
228
     * If this is false, the class cannot be created in the CMS by regular content authors, only by ADMINs.
229
     * @var boolean
230
     * @config
231
     */
232
    private static $can_create = true;
233
234
    /**
235
     * Icon to use in the CMS page tree. This should be the full filename, relative to the webroot.
236
     * Also supports custom CSS rule contents (applied to the correct selector for the tree UI implementation).
237
     *
238
     * @see CMSMain::generateTreeStylingCSS()
239
     * @config
240
     * @var string
241
     */
242
    private static $icon = null;
243
244
    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...
245
        Hierarchy::class,
246
        Versioned::class,
247
        SiteTreeLinkTracking::class,
248
        InheritedPermissionsExtension::class,
249
    ];
250
251
    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...
252
        'Title',
253
        'Content',
254
    );
255
256
    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...
257
        'URLSegment' => 'URL'
258
    );
259
260
    /**
261
     * @config
262
     */
263
    private static $nested_urls = true;
264
265
    /**
266
     * @config
267
    */
268
    private static $create_default_pages = true;
269
270
    /**
271
     * This controls whether of not extendCMSFields() is called by getCMSFields.
272
     */
273
    private static $runCMSFieldsExtensions = true;
274
275
    /**
276
     * @config
277
     * @var boolean
278
     */
279
    private static $enforce_strict_hierarchy = true;
280
281
    /**
282
     * The value used for the meta generator tag. Leave blank to omit the tag.
283
     *
284
     * @config
285
     * @var string
286
     */
287
    private static $meta_generator = 'SilverStripe - http://silverstripe.org';
288
289
    protected $_cache_statusFlags = null;
290
291
    /**
292
     * Plural form for SiteTree / Page classes. Not inherited by subclasses.
293
     *
294
     * @config
295
     * @var string
296
     */
297
    private static $base_plural_name = 'Pages';
298
299
    /**
300
     * Plural form for SiteTree / Page classes. Not inherited by subclasses.
301
     *
302
     * @config
303
     * @var string
304
     */
305
    private static $base_singular_name = 'Page';
306
307
    /**
308
     * Description of the class functionality, typically shown to a user
309
     * when selecting which page type to create. Translated through {@link provideI18nEntities()}.
310
     *
311
     * @see SiteTree::classDescription()
312
     * @see SiteTree::i18n_classDescription()
313
     *
314
     * @config
315
     * @var string
316
     */
317
    private static $description = null;
318
319
    /**
320
     * Description for Page and SiteTree classes, but not inherited by subclasses.
321
     * override SiteTree::$description in subclasses instead.
322
     *
323
     * @see SiteTree::classDescription()
324
     * @see SiteTree::i18n_classDescription()
325
     *
326
     * @config
327
     * @var string
328
     */
329
    private static $base_description = 'Generic content page';
330
331
    /**
332
     * Fetches the {@link SiteTree} object that maps to a link.
333
     *
334
     * If you have enabled {@link SiteTree::config()->nested_urls} on this site, then you can use a nested link such as
335
     * "about-us/staff/", and this function will traverse down the URL chain and grab the appropriate link.
336
     *
337
     * Note that if no model can be found, this method will fall over to a extended alternateGetByLink method provided
338
     * by a extension attached to {@link SiteTree}
339
     *
340
     * @param string $link  The link of the page to search for
341
     * @param bool   $cache True (default) to use caching, false to force a fresh search from the database
342
     * @return SiteTree
343
     */
344
    public static function get_by_link($link, $cache = true)
345
    {
346
        if (trim($link, '/')) {
347
            $link = trim(Director::makeRelative($link), '/');
348
        } else {
349
            $link = RootURLController::get_homepage_link();
350
        }
351
352
        $parts = preg_split('|/+|', $link);
353
354
        // Grab the initial root level page to traverse down from.
355
        $URLSegment = array_shift($parts);
356
        $conditions = array('"SiteTree"."URLSegment"' => rawurlencode($URLSegment));
357
        if (self::config()->nested_urls) {
358
            $conditions[] = array('"SiteTree"."ParentID"' => 0);
359
        }
360
        /** @var SiteTree $sitetree */
361
        $sitetree = DataObject::get_one(self::class, $conditions, $cache);
362
363
        /// Fall back on a unique URLSegment for b/c.
364
        if (!$sitetree
365
            && self::config()->nested_urls
366
            && $sitetree = DataObject::get_one(self::class, array(
367
                '"SiteTree"."URLSegment"' => $URLSegment
368
            ), $cache)
369
        ) {
370
            return $sitetree;
371
        }
372
373
        // Attempt to grab an alternative page from extensions.
374
        if (!$sitetree) {
375
            $parentID = self::config()->nested_urls ? 0 : null;
376
377 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...
378
                foreach ($alternatives as $alternative) {
379
                    if ($alternative) {
380
                        $sitetree = $alternative;
381
                    }
382
                }
383
            }
384
385
            if (!$sitetree) {
386
                return null;
387
            }
388
        }
389
390
        // Check if we have any more URL parts to parse.
391
        if (!self::config()->nested_urls || !count($parts)) {
392
            return $sitetree;
393
        }
394
395
        // Traverse down the remaining URL segments and grab the relevant SiteTree objects.
396
        foreach ($parts as $segment) {
397
            $next = DataObject::get_one(
398
                self::class,
399
                array(
400
                    '"SiteTree"."URLSegment"' => $segment,
401
                    '"SiteTree"."ParentID"' => $sitetree->ID
402
                ),
403
                $cache
404
            );
405
406
            if (!$next) {
407
                $parentID = (int) $sitetree->ID;
408
409 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...
410
                    foreach ($alternatives as $alternative) {
411
                        if ($alternative) {
412
                            $next = $alternative;
413
                        }
414
                    }
415
                }
416
417
                if (!$next) {
418
                    return null;
419
                }
420
            }
421
422
            $sitetree->destroy();
423
            $sitetree = $next;
424
        }
425
426
        return $sitetree;
427
    }
428
429
    /**
430
     * Return a subclass map of SiteTree that shouldn't be hidden through {@link SiteTree::$hide_ancestor}
431
     *
432
     * @return array
433
     */
434
    public static function page_type_classes()
435
    {
436
        $classes = ClassInfo::getValidSubClasses();
437
438
        $baseClassIndex = array_search(self::class, $classes);
439
        if ($baseClassIndex !== false) {
440
            unset($classes[$baseClassIndex]);
441
        }
442
443
        $kill_ancestors = array();
444
445
        // figure out if there are any classes we don't want to appear
446
        foreach ($classes as $class) {
447
            $instance = singleton($class);
448
449
            // do any of the progeny want to hide an ancestor?
450
            if ($ancestor_to_hide = $instance->stat('hide_ancestor')) {
451
                // note for killing later
452
                $kill_ancestors[] = $ancestor_to_hide;
453
            }
454
        }
455
456
        // If any of the descendents don't want any of the elders to show up, cruelly render the elders surplus to
457
        // requirements
458
        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...
459
            $kill_ancestors = array_unique($kill_ancestors);
460
            foreach ($kill_ancestors as $mark) {
461
                // unset from $classes
462
                $idx = array_search($mark, $classes, true);
463
                if ($idx !== false) {
464
                    unset($classes[$idx]);
465
                }
466
            }
467
        }
468
469
        return $classes;
470
    }
471
472
    /**
473
     * Replace a "[sitetree_link id=n]" shortcode with a link to the page with the corresponding ID.
474
     *
475
     * @param array      $arguments
476
     * @param string     $content
477
     * @param ShortcodeParser $parser
478
     * @return string
479
     */
480
    public static function link_shortcode_handler($arguments, $content = null, $parser = null)
481
    {
482
        if (!isset($arguments['id']) || !is_numeric($arguments['id'])) {
483
            return null;
484
        }
485
486
        /** @var SiteTree $page */
487
        if (!($page = DataObject::get_by_id(self::class, $arguments['id']))         // Get the current page by ID.
488
            && !($page = Versioned::get_latest_version(self::class, $arguments['id'])) // Attempt link to old version.
489
        ) {
490
             return null; // There were no suitable matches at all.
491
        }
492
493
        /** @var SiteTree $page */
494
        $link = Convert::raw2att($page->Link());
495
496
        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...
497
            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...
498
        } else {
499
            return $link;
500
        }
501
    }
502
503
    /**
504
     * Return the link for this {@link SiteTree} object, with the {@link Director::baseURL()} included.
505
     *
506
     * @param string $action Optional controller action (method).
507
     *                       Note: URI encoding of this parameter is applied automatically through template casting,
508
     *                       don't encode the passed parameter. Please use {@link Controller::join_links()} instead to
509
     *                       append GET parameters.
510
     * @return string
511
     */
512
    public function Link($action = null)
513
    {
514
        return Controller::join_links(Director::baseURL(), $this->RelativeLink($action));
515
    }
516
517
    /**
518
     * Get the absolute URL for this page, including protocol and host.
519
     *
520
     * @param string $action See {@link Link()}
521
     * @return string
522
     */
523
    public function AbsoluteLink($action = null)
524
    {
525
        if ($this->hasMethod('alternateAbsoluteLink')) {
526
            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...
527
        } else {
528
            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 528 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...
529
        }
530
    }
531
532
    /**
533
     * Base link used for previewing. Defaults to absolute URL, in order to account for domain changes, e.g. on multi
534
     * site setups. Does not contain hints about the stage, see {@link SilverStripeNavigator} for details.
535
     *
536
     * @param string $action See {@link Link()}
537
     * @return string
538
     */
539
    public function PreviewLink($action = null)
540
    {
541
        if ($this->hasMethod('alternatePreviewLink')) {
542
            Deprecation::notice('5.0', 'Use updatePreviewLink or override PreviewLink method');
543
            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...
544
        }
545
546
        $link = $this->AbsoluteLink($action);
547
        $this->extend('updatePreviewLink', $link, $action);
548
        return $link;
549
    }
550
551
    public function getMimeType()
552
    {
553
        return 'text/html';
554
    }
555
556
    /**
557
     * Return the link for this {@link SiteTree} object relative to the SilverStripe root.
558
     *
559
     * By default, if this page is the current home page, and there is no action specified then this will return a link
560
     * to the root of the site. However, if you set the $action parameter to TRUE then the link will not be rewritten
561
     * and returned in its full form.
562
     *
563
     * @uses RootURLController::get_homepage_link()
564
     *
565
     * @param string $action See {@link Link()}
566
     * @return string
567
     */
568
    public function RelativeLink($action = null)
569
    {
570
        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...
571
            $parent = $this->Parent();
572
            // If page is removed select parent from version history (for archive page view)
573
            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...
574
                $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...
575
            }
576
            $base = $parent->RelativeLink($this->URLSegment);
577
        } 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...
578
            // Unset base for root-level homepages.
579
            // Note: Homepages with action parameters (or $action === true)
580
            // need to retain their URLSegment.
581
            $base = null;
582
        } else {
583
            $base = $this->URLSegment;
584
        }
585
586
        $this->extend('updateRelativeLink', $base, $action);
587
588
        // Legacy support: If $action === true, retain URLSegment for homepages,
589
        // but don't append any action
590
        if ($action === true) {
591
            $action = null;
592
        }
593
594
        return Controller::join_links($base, '/', $action);
595
    }
596
597
    /**
598
     * Get the absolute URL for this page on the Live site.
599
     *
600
     * @param bool $includeStageEqualsLive Whether to append the URL with ?stage=Live to force Live mode
601
     * @return string
602
     */
603
    public function getAbsoluteLiveLink($includeStageEqualsLive = true)
604
    {
605
        $oldReadingMode = Versioned::get_reading_mode();
606
        Versioned::set_stage(Versioned::LIVE);
607
        /** @var SiteTree $live */
608
        $live = Versioned::get_one_by_stage(self::class, Versioned::LIVE, array(
609
            '"SiteTree"."ID"' => $this->ID
610
        ));
611
        if ($live) {
612
            $link = $live->AbsoluteLink();
613
            if ($includeStageEqualsLive) {
614
                $link = Controller::join_links($link, '?stage=Live');
615
            }
616
        } else {
617
            $link = null;
618
        }
619
620
        Versioned::set_reading_mode($oldReadingMode);
621
        return $link;
622
    }
623
624
    /**
625
     * Generates a link to edit this page in the CMS.
626
     *
627
     * @return string
628
     */
629
    public function CMSEditLink()
630
    {
631
        $link = Controller::join_links(
632
            CMSPageEditController::singleton()->Link('show'),
633
            $this->ID
634
        );
635
        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 635 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...
636
    }
637
638
639
    /**
640
     * Return a CSS identifier generated from this page's link.
641
     *
642
     * @return string The URL segment
643
     */
644
    public function ElementName()
645
    {
646
        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...
647
    }
648
649
    /**
650
     * Returns true if this is the currently active page being used to handle this request.
651
     *
652
     * @return bool
653
     */
654
    public function isCurrent()
655
    {
656
        $currentPage = Director::get_current_page();
657
        if ($currentPage instanceof ContentController) {
658
            $currentPage = $currentPage->data();
659
        }
660
        if ($currentPage instanceof SiteTree) {
661
            return $currentPage === $this || $currentPage->ID === $this->ID;
662
        }
663
        return false;
664
    }
665
666
    /**
667
     * Check if this page is in the currently active section (e.g. it is either current or one of its children is
668
     * currently being viewed).
669
     *
670
     * @return bool
671
     */
672
    public function isSection()
673
    {
674
        return $this->isCurrent() || (
675
            Director::get_current_page() instanceof SiteTree && in_array($this->ID, Director::get_current_page()->getAncestors()->column())
676
        );
677
    }
678
679
    /**
680
     * Check if the parent of this page has been removed (or made otherwise unavailable), and is still referenced by
681
     * this child. Any such orphaned page may still require access via the CMS, but should not be shown as accessible
682
     * to external users.
683
     *
684
     * @return bool
685
     */
686
    public function isOrphaned()
687
    {
688
        // Always false for root pages
689
        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...
690
            return false;
691
        }
692
693
        // Parent must exist and not be an orphan itself
694
        $parent = $this->Parent();
695
        return !$parent || !$parent->exists() || $parent->isOrphaned();
696
    }
697
698
    /**
699
     * Return "link" or "current" depending on if this is the {@link SiteTree::isCurrent()} current page.
700
     *
701
     * @return string
702
     */
703
    public function LinkOrCurrent()
704
    {
705
        return $this->isCurrent() ? 'current' : 'link';
706
    }
707
708
    /**
709
     * Return "link" or "section" depending on if this is the {@link SiteTree::isSeciton()} current section.
710
     *
711
     * @return string
712
     */
713
    public function LinkOrSection()
714
    {
715
        return $this->isSection() ? 'section' : 'link';
716
    }
717
718
    /**
719
     * Return "link", "current" or "section" depending on if this page is the current page, or not on the current page
720
     * but in the current section.
721
     *
722
     * @return string
723
     */
724
    public function LinkingMode()
725
    {
726
        if ($this->isCurrent()) {
727
            return 'current';
728
        } elseif ($this->isSection()) {
729
            return 'section';
730
        } else {
731
            return 'link';
732
        }
733
    }
734
735
    /**
736
     * Check if this page is in the given current section.
737
     *
738
     * @param string $sectionName Name of the section to check
739
     * @return bool True if we are in the given section
740
     */
741
    public function InSection($sectionName)
742
    {
743
        $page = Director::get_current_page();
744
        while ($page && $page->exists()) {
745
            if ($sectionName == $page->URLSegment) {
746
                return true;
747
            }
748
            $page = $page->Parent();
749
        }
750
        return false;
751
    }
752
753
    /**
754
     * Reset Sort on duped page
755
     *
756
     * @param SiteTree $original
757
     * @param bool $doWrite
758
     */
759
    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...
760
    {
761
        $this->Sort = 0;
762
    }
763
764
    /**
765
     * Duplicates each child of this node recursively and returns the top-level duplicate node.
766
     *
767
     * @return static The duplicated object
768
     */
769
    public function duplicateWithChildren()
770
    {
771
        /** @var SiteTree $clone */
772
        $clone = $this->duplicate();
773
        $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...
774
775
        if ($children) {
776
            /** @var SiteTree $child */
777
            $sort = 0;
778
            foreach ($children as $child) {
779
                $childClone = $child->duplicateWithChildren();
780
                $childClone->ParentID = $clone->ID;
781
                //retain sort order by manually setting sort values
782
                $childClone->Sort = ++$sort;
783
                $childClone->write();
784
            }
785
        }
786
787
        return $clone;
788
    }
789
790
    /**
791
     * Duplicate this node and its children as a child of the node with the given ID
792
     *
793
     * @param int $id ID of the new node's new parent
794
     */
795
    public function duplicateAsChild($id)
796
    {
797
        /** @var SiteTree $newSiteTree */
798
        $newSiteTree = $this->duplicate();
799
        $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...
800
        $newSiteTree->Sort = 0;
801
        $newSiteTree->write();
802
    }
803
804
    /**
805
     * Return a breadcrumb trail to this page. Excludes "hidden" pages (with ShowInMenus=0) by default.
806
     *
807
     * @param int $maxDepth The maximum depth to traverse.
808
     * @param boolean $unlinked Whether to link page titles.
809
     * @param boolean|string $stopAtPageType ClassName of a page to stop the upwards traversal.
810
     * @param boolean $showHidden Include pages marked with the attribute ShowInMenus = 0
811
     * @return string The breadcrumb trail.
812
     */
813
    public function Breadcrumbs($maxDepth = 20, $unlinked = false, $stopAtPageType = false, $showHidden = false)
814
    {
815
        $pages = $this->getBreadcrumbItems($maxDepth, $stopAtPageType, $showHidden);
816
        $template = new SSViewer('BreadcrumbsTemplate');
817
        return $template->process($this->customise(new ArrayData(array(
818
            "Pages" => $pages,
819
            "Unlinked" => $unlinked
820
        ))));
821
    }
822
823
824
    /**
825
     * Returns a list of breadcrumbs for the current page.
826
     *
827
     * @param int $maxDepth The maximum depth to traverse.
828
     * @param boolean|string $stopAtPageType ClassName of a page to stop the upwards traversal.
829
     * @param boolean $showHidden Include pages marked with the attribute ShowInMenus = 0
830
     *
831
     * @return ArrayList
832
    */
833
    public function getBreadcrumbItems($maxDepth = 20, $stopAtPageType = false, $showHidden = false)
834
    {
835
        $page = $this;
836
        $pages = array();
837
838
        while ($page
839
            && $page->exists()
840
            && (!$maxDepth || count($pages) < $maxDepth)
841
            && (!$stopAtPageType || $page->ClassName != $stopAtPageType)
842
        ) {
843
            if ($showHidden || $page->ShowInMenus || ($page->ID == $this->ID)) {
844
                $pages[] = $page;
845
            }
846
847
            $page = $page->Parent();
848
        }
849
850
        return new ArrayList(array_reverse($pages));
851
    }
852
853
854
    /**
855
     * Make this page a child of another page.
856
     *
857
     * If the parent page does not exist, resolve it to a valid ID before updating this page's reference.
858
     *
859
     * @param SiteTree|int $item Either the parent object, or the parent ID
860
     */
861
    public function setParent($item)
862
    {
863
        if (is_object($item)) {
864
            if (!$item->exists()) {
865
                $item->write();
866
            }
867
            $this->setField("ParentID", $item->ID);
868
        } else {
869
            $this->setField("ParentID", $item);
870
        }
871
    }
872
873
    /**
874
     * Get the parent of this page.
875
     *
876
     * @return SiteTree Parent of this page
877
     */
878
    public function getParent()
879
    {
880
        if ($parentID = $this->getField("ParentID")) {
881
            return DataObject::get_by_id(self::class, $parentID);
882
        }
883
        return null;
884
    }
885
886
    /**
887
     * Return a string of the form "parent - page" or "grandparent - parent - page" using page titles
888
     *
889
     * @param int $level The maximum amount of levels to traverse.
890
     * @param string $separator Seperating string
891
     * @return string The resulting string
892
     */
893
    public function NestedTitle($level = 2, $separator = " - ")
894
    {
895
        $item = $this;
896
        $parts = [];
897
        while ($item && $level > 0) {
898
            $parts[] = $item->Title;
899
            $item = $item->getParent();
900
            $level--;
901
        }
902
        return implode($separator, array_reverse($parts));
903
    }
904
905
    /**
906
     * This function should return true if the current user can execute this action. It can be overloaded to customise
907
     * the security model for an application.
908
     *
909
     * Slightly altered from parent behaviour in {@link DataObject->can()}:
910
     * - Checks for existence of a method named "can<$perm>()" on the object
911
     * - Calls decorators and only returns for FALSE "vetoes"
912
     * - Falls back to {@link Permission::check()}
913
     * - Does NOT check for many-many relations named "Can<$perm>"
914
     *
915
     * @uses DataObjectDecorator->can()
916
     *
917
     * @param string $perm The permission to be checked, such as 'View'
918
     * @param Member $member The member whose permissions need checking. Defaults to the currently logged in user.
919
     * @param array $context Context argument for canCreate()
920
     * @return bool True if the the member is allowed to do the given action
921
     */
922
    public function can($perm, $member = null, $context = array())
923
    {
924
        if (!$member) {
925
            $member = Member::currentUser();
926
        }
927
928
        if ($member && Permission::checkMember($member, "ADMIN")) {
929
            return true;
930
        }
931
932
        if (is_string($perm) && method_exists($this, 'can' . ucfirst($perm))) {
933
            $method = 'can' . ucfirst($perm);
934
            return $this->$method($member);
935
        }
936
937
        $results = $this->extend('can', $member);
938
        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...
939
            if (!min($results)) {
940
                return false;
941
            }
942
        }
943
944
        return ($member && Permission::checkMember($member, $perm));
945
    }
946
947
    /**
948
     * This function should return true if the current user can add children to this page. It can be overloaded to
949
     * customise the security model for an application.
950
     *
951
     * Denies permission if any of the following conditions is true:
952
     * - alternateCanAddChildren() on a extension returns false
953
     * - canEdit() is not granted
954
     * - There are no classes defined in {@link $allowed_children}
955
     *
956
     * @uses SiteTreeExtension->canAddChildren()
957
     * @uses canEdit()
958
     * @uses $allowed_children
959
     *
960
     * @param Member|int $member
961
     * @return bool True if the current user can add children
962
     */
963
    public function canAddChildren($member = null)
964
    {
965
        // Disable adding children to archived pages
966
        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...
967
            return false;
968
        }
969
970
        if (!$member) {
971
            $member = Member::currentUser();
972
        }
973
974
        // Standard mechanism for accepting permission changes from extensions
975
        $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...
976
        if ($extended !== null) {
977
            return $extended;
978
        }
979
980
        // Default permissions
981
        if ($member && Permission::checkMember($member, "ADMIN")) {
982
            return true;
983
        }
984
985
        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 963 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...
986
    }
987
988
    /**
989
     * This function should return true if the current user can view this page. It can be overloaded to customise the
990
     * security model for an application.
991
     *
992
     * Denies permission if any of the following conditions is true:
993
     * - canView() on any extension returns false
994
     * - "CanViewType" directive is set to "Inherit" and any parent page return false for canView()
995
     * - "CanViewType" directive is set to "LoggedInUsers" and no user is logged in
996
     * - "CanViewType" directive is set to "OnlyTheseUsers" and user is not in the given groups
997
     *
998
     * @uses DataExtension->canView()
999
     * @uses ViewerGroups()
1000
     *
1001
     * @param Member $member
1002
     * @return bool True if the current user can view this page
1003
     */
1004
    public function canView($member = null)
1005
    {
1006
        if (!$member) {
1007
            $member = Member::currentUser();
1008
        }
1009
1010
        // Standard mechanism for accepting permission changes from extensions
1011
        $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...
1012
        if ($extended !== null) {
1013
            return $extended;
1014
        }
1015
1016
        // admin override
1017
        if ($member && Permission::checkMember($member, array("ADMIN", "SITETREE_VIEW_ALL"))) {
1018
            return true;
1019
        }
1020
1021
        // Orphaned pages (in the current stage) are unavailable, except for admins via the CMS
1022
        if ($this->isOrphaned()) {
1023
            return false;
1024
        }
1025
1026
        // Note: getInheritedPermissions() is disused in this instance
1027
        // to allow parent canView extensions to influence subpage canView()
1028
1029
        // check for empty spec
1030
        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...
1031
            return true;
1032
        }
1033
1034
        // check for inherit
1035
        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...
1036
            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...
1037
                return $this->Parent()->canView($member);
1038
            } else {
1039
                return $this->getSiteConfig()->canViewPages($member);
1040
            }
1041
        }
1042
1043
        // check for any logged-in users
1044
        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...
1045
            return true;
1046
        }
1047
1048
        // check for specific groups
1049
        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...
1050
            && $member
1051
            && $member->inGroups($this->ViewerGroups())
1052
        ) {
1053
            return true;
1054
        }
1055
1056
        return false;
1057
    }
1058
1059
    /**
1060
     * Check if this page can be published
1061
     *
1062
     * @param Member $member
1063
     * @return bool
1064
     */
1065 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...
1066
    {
1067
        if (!$member) {
1068
            $member = Member::currentUser();
1069
        }
1070
1071
        // Check extension
1072
        $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...
1073
        if ($extended !== null) {
1074
            return $extended;
1075
        }
1076
1077
        if (Permission::checkMember($member, "ADMIN")) {
1078
            return true;
1079
        }
1080
1081
        // Default to relying on edit permission
1082
        return $this->canEdit($member);
1083
    }
1084
1085
    /**
1086
     * This function should return true if the current user can delete this page. It can be overloaded to customise the
1087
     * security model for an application.
1088
     *
1089
     * Denies permission if any of the following conditions is true:
1090
     * - canDelete() returns false on any extension
1091
     * - canEdit() returns false
1092
     * - any descendant page returns false for canDelete()
1093
     *
1094
     * @uses canDelete()
1095
     * @uses SiteTreeExtension->canDelete()
1096
     * @uses canEdit()
1097
     *
1098
     * @param Member $member
1099
     * @return bool True if the current user can delete this page
1100
     */
1101
    public function canDelete($member = null)
1102
    {
1103
        if (!$member) {
1104
            $member = Member::currentUser();
1105
        }
1106
1107
        // Standard mechanism for accepting permission changes from extensions
1108
        $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...
1109
        if ($extended !== null) {
1110
            return $extended;
1111
        }
1112
1113
        if (!$member) {
1114
            return false;
1115
        }
1116
1117
        // Default permission check
1118
        if (Permission::checkMember($member, array("ADMIN", "SITETREE_EDIT_ALL"))) {
1119
            return true;
1120
        }
1121
1122
        // Check inherited permissions
1123
        return static::getPermissionChecker()
1124
            ->canDelete($this->ID, $member);
1125
    }
1126
1127
    /**
1128
     * This function should return true if the current user can create new pages of this class, regardless of class. It
1129
     * can be overloaded to customise the security model for an application.
1130
     *
1131
     * By default, permission to create at the root level is based on the SiteConfig configuration, and permission to
1132
     * create beneath a parent is based on the ability to edit that parent page.
1133
     *
1134
     * Use {@link canAddChildren()} to control behaviour of creating children under this page.
1135
     *
1136
     * @uses $can_create
1137
     * @uses DataExtension->canCreate()
1138
     *
1139
     * @param Member $member
1140
     * @param array $context Optional array which may contain array('Parent' => $parentObj)
1141
     *                       If a parent page is known, it will be checked for validity.
1142
     *                       If omitted, it will be assumed this is to be created as a top level page.
1143
     * @return bool True if the current user can create pages on this class.
1144
     */
1145
    public function canCreate($member = null, $context = array())
1146
    {
1147
        if (!$member) {
1148
            $member = Member::currentUser();
1149
        }
1150
1151
        // Check parent (custom canCreate option for SiteTree)
1152
        // Block children not allowed for this parent type
1153
        $parent = isset($context['Parent']) ? $context['Parent'] : null;
1154
        if ($parent && !in_array(static::class, $parent->allowedChildren())) {
1155
            return false;
1156
        }
1157
1158
        // Standard mechanism for accepting permission changes from extensions
1159
        $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...
1160
        if ($extended !== null) {
1161
            return $extended;
1162
        }
1163
1164
        // Check permission
1165
        if ($member && Permission::checkMember($member, "ADMIN")) {
1166
            return true;
1167
        }
1168
1169
        // Fall over to inherited permissions
1170
        if ($parent && $parent->exists()) {
1171
            return $parent->canAddChildren($member);
1172
        } else {
1173
            // This doesn't necessarily mean we are creating a root page, but that
1174
            // we don't know if there is a parent, so default to this permission
1175
            return SiteConfig::current_site_config()->canCreateTopLevel($member);
1176
        }
1177
    }
1178
1179
    /**
1180
     * This function should return true if the current user can edit this page. It can be overloaded to customise the
1181
     * security model for an application.
1182
     *
1183
     * Denies permission if any of the following conditions is true:
1184
     * - canEdit() on any extension returns false
1185
     * - canView() return false
1186
     * - "CanEditType" directive is set to "Inherit" and any parent page return false for canEdit()
1187
     * - "CanEditType" directive is set to "LoggedInUsers" and no user is logged in or doesn't have the
1188
     *   CMS_Access_CMSMAIN permission code
1189
     * - "CanEditType" directive is set to "OnlyTheseUsers" and user is not in the given groups
1190
     *
1191
     * @uses canView()
1192
     * @uses EditorGroups()
1193
     * @uses DataExtension->canEdit()
1194
     *
1195
     * @param Member $member Set to false if you want to explicitly test permissions without a valid user (useful for
1196
     *                       unit tests)
1197
     * @return bool True if the current user can edit this page
1198
     */
1199 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...
1200
    {
1201
        if (!$member) {
1202
            $member = Member::currentUser();
1203
        }
1204
1205
        // Standard mechanism for accepting permission changes from extensions
1206
        $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...
1207
        if ($extended !== null) {
1208
            return $extended;
1209
        }
1210
1211
        // Default permissions
1212
        if (Permission::checkMember($member, "SITETREE_EDIT_ALL")) {
1213
            return true;
1214
        }
1215
1216
        // Check inherited permissions
1217
        return static::getPermissionChecker()
1218
            ->canEdit($this->ID, $member);
1219
    }
1220
1221
    /**
1222
     * Stub method to get the site config, unless the current class can provide an alternate.
1223
     *
1224
     * @return SiteConfig
1225
     */
1226
    public function getSiteConfig()
1227
    {
1228
        $configs = $this->invokeWithExtensions('alternateSiteConfig');
1229
        foreach (array_filter($configs) as $config) {
1230
            return $config;
1231
        }
1232
1233
        return SiteConfig::current_site_config();
1234
    }
1235
1236
    /**
1237
     * @return PermissionChecker
1238
     */
1239
    public static function getPermissionChecker()
1240
    {
1241
        return Injector::inst()->get(PermissionChecker::class.'.sitetree');
1242
    }
1243
1244
    /**
1245
     * Collate selected descendants of this page.
1246
     *
1247
     * {@link $condition} will be evaluated on each descendant, and if it is succeeds, that item will be added to the
1248
     * $collator array.
1249
     *
1250
     * @param string $condition The PHP condition to be evaluated. The page will be called $item
1251
     * @param array  $collator  An array, passed by reference, to collect all of the matching descendants.
1252
     * @return bool
1253
     */
1254
    public function collateDescendants($condition, &$collator)
1255
    {
1256
        $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...
1257
        if ($children) {
1258
            foreach ($children as $item) {
1259
                if (eval("return $condition;")) {
1260
                    $collator[] = $item;
1261
                }
1262
                /** @var SiteTree $item */
1263
                $item->collateDescendants($condition, $collator);
1264
            }
1265
            return true;
1266
        }
1267
        return false;
1268
    }
1269
1270
    /**
1271
     * Return the title, description, keywords and language metatags.
1272
     *
1273
     * @todo Move <title> tag in separate getter for easier customization and more obvious usage
1274
     *
1275
     * @param bool $includeTitle Show default <title>-tag, set to false for custom templating
1276
     * @return string The XHTML metatags
1277
     */
1278
    public function MetaTags($includeTitle = true)
1279
    {
1280
        $tags = array();
1281
        if ($includeTitle && strtolower($includeTitle) != 'false') {
1282
            $tags[] = FormField::create_tag('title', array(), $this->obj('Title')->forTemplate());
1283
        }
1284
1285
        $generator = trim(Config::inst()->get(self::class, 'meta_generator'));
1286
        if (!empty($generator)) {
1287
            $tags[] = FormField::create_tag('meta', array(
1288
                'name' => 'generator',
1289
                'content' => $generator,
1290
            ));
1291
        }
1292
1293
        $charset = ContentNegotiator::config()->uninherited('encoding');
1294
        $tags[] = FormField::create_tag('meta', array(
1295
            'http-equiv' => 'Content-Type',
1296
            'content' => 'text/html; charset=' . $charset,
1297
        ));
1298
        if ($this->MetaDescription) {
1299
            $tags[] = FormField::create_tag('meta', array(
1300
                'name' => 'description',
1301
                'content' => $this->MetaDescription,
1302
            ));
1303
        }
1304
1305
        if (Permission::check('CMS_ACCESS_CMSMain')
1306
            && !$this instanceof ErrorPage
1307
            && $this->ID > 0
1308
        ) {
1309
            $tags[] = FormField::create_tag('meta', array(
1310
                'name' => 'x-page-id',
1311
                'content' => $this->obj('ID')->forTemplate(),
1312
            ));
1313
            $tags[] = FormField::create_tag('meta', array(
1314
                'name' => 'x-cms-edit-link',
1315
                'content' => $this->obj('CMSEditLink')->forTemplate(),
1316
            ));
1317
        }
1318
1319
        $tags = implode("\n", $tags);
1320
        if ($this->ExtraMeta) {
1321
            $tags .= $this->obj('ExtraMeta')->forTemplate();
1322
        }
1323
1324
        $this->extend('MetaTags', $tags);
1325
1326
        return $tags;
1327
    }
1328
1329
    /**
1330
     * Returns the object that contains the content that a user would associate with this page.
1331
     *
1332
     * Ordinarily, this is just the page itself, but for example on RedirectorPages or VirtualPages ContentSource() will
1333
     * return the page that is linked to.
1334
     *
1335
     * @return $this
1336
     */
1337
    public function ContentSource()
1338
    {
1339
        return $this;
1340
    }
1341
1342
    /**
1343
     * Add default records to database.
1344
     *
1345
     * This function is called whenever the database is built, after the database tables have all been created. Overload
1346
     * this to add default records when the database is built, but make sure you call parent::requireDefaultRecords().
1347
     */
1348
    public function requireDefaultRecords()
1349
    {
1350
        parent::requireDefaultRecords();
1351
1352
        // default pages
1353
        if (static::class == self::class && $this->config()->create_default_pages) {
1354
            if (!SiteTree::get_by_link(RootURLController::config()->default_homepage_link)) {
1355
                $homepage = new Page();
1356
                $homepage->Title = _t(__CLASS__.'.DEFAULTHOMETITLE', 'Home');
1357
                $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>');
1358
                $homepage->URLSegment = RootURLController::config()->default_homepage_link;
1359
                $homepage->Sort = 1;
1360
                $homepage->write();
1361
                $homepage->copyVersionToStage(Versioned::DRAFT, Versioned::LIVE);
1362
                $homepage->flushCache();
1363
                DB::alteration_message('Home page created', 'created');
1364
            }
1365
1366
            if (DB::query("SELECT COUNT(*) FROM \"SiteTree\"")->value() == 1) {
1367
                $aboutus = new Page();
1368
                $aboutus->Title = _t(__CLASS__.'.DEFAULTABOUTTITLE', 'About Us');
1369
                $aboutus->Content = _t(
1370
                    'SilverStripe\\CMS\\Model\\SiteTree.DEFAULTABOUTCONTENT',
1371
                    '<p>You can fill this page out with your own content, or delete it and create your own pages.</p>'
1372
                );
1373
                $aboutus->Sort = 2;
1374
                $aboutus->write();
1375
                $aboutus->copyVersionToStage(Versioned::DRAFT, Versioned::LIVE);
1376
                $aboutus->flushCache();
1377
                DB::alteration_message('About Us page created', 'created');
1378
1379
                $contactus = new Page();
1380
                $contactus->Title = _t(__CLASS__.'.DEFAULTCONTACTTITLE', 'Contact Us');
1381
                $contactus->Content = _t(
1382
                    'SilverStripe\\CMS\\Model\\SiteTree.DEFAULTCONTACTCONTENT',
1383
                    '<p>You can fill this page out with your own content, or delete it and create your own pages.</p>'
1384
                );
1385
                $contactus->Sort = 3;
1386
                $contactus->write();
1387
                $contactus->copyVersionToStage(Versioned::DRAFT, Versioned::LIVE);
1388
                $contactus->flushCache();
1389
                DB::alteration_message('Contact Us page created', 'created');
1390
            }
1391
        }
1392
    }
1393
1394
    protected function onBeforeWrite()
1395
    {
1396
        parent::onBeforeWrite();
1397
1398
        // If Sort hasn't been set, make this page come after it's siblings
1399
        if (!$this->Sort) {
1400
            $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...
1401
            $this->Sort = DB::prepared_query(
1402
                "SELECT MAX(\"Sort\") + 1 FROM \"SiteTree\" WHERE \"ParentID\" = ?",
1403
                array($parentID)
1404
            )->value();
1405
        }
1406
1407
        // If there is no URLSegment set, generate one from Title
1408
        $defaultSegment = $this->generateURLSegment(_t(
1409
            'SilverStripe\\CMS\\Controllers\\CMSMain.NEWPAGE',
1410
            'New {pagetype}',
1411
            array('pagetype' => $this->i18n_singular_name())
1412
        ));
1413
        if ((!$this->URLSegment || $this->URLSegment == $defaultSegment) && $this->Title) {
1414
            $this->URLSegment = $this->generateURLSegment($this->Title);
1415
        } elseif ($this->isChanged('URLSegment', 2)) {
1416
            // Do a strict check on change level, to avoid double encoding caused by
1417
            // bogus changes through forceChange()
1418
            $filter = URLSegmentFilter::create();
1419
            $this->URLSegment = $filter->filter($this->URLSegment);
1420
            // If after sanitising there is no URLSegment, give it a reasonable default
1421
            if (!$this->URLSegment) {
1422
                $this->URLSegment = "page-$this->ID";
1423
            }
1424
        }
1425
1426
        // Ensure that this object has a non-conflicting URLSegment value.
1427
        $count = 2;
1428
        while (!$this->validURLSegment()) {
1429
            $this->URLSegment = preg_replace('/-[0-9]+$/', null, $this->URLSegment) . '-' . $count;
1430
            $count++;
1431
        }
1432
1433
        $this->syncLinkTracking();
1434
1435
        // Check to see if we've only altered fields that shouldn't affect versioning
1436
        $fieldsIgnoredByVersioning = array('HasBrokenLink', 'Status', 'HasBrokenFile', 'ToDo', 'VersionID', 'SaveCount');
1437
        $changedFields = array_keys($this->getChangedFields(true, 2));
1438
1439
        // This more rigorous check is inline with the test that write() does to decide whether or not to write to the
1440
        // DB. We use that to avoid cluttering the system with a migrateVersion() call that doesn't get used
1441
        $oneChangedFields = array_keys($this->getChangedFields(true, 1));
1442
1443
        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...
1444
            // This will have the affect of preserving the versioning
1445
            $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...
1446
        }
1447
    }
1448
1449
    /**
1450
     * Trigger synchronisation of link tracking
1451
     *
1452
     * {@see SiteTreeLinkTracking::augmentSyncLinkTracking}
1453
     */
1454
    public function syncLinkTracking()
1455
    {
1456
        $this->extend('augmentSyncLinkTracking');
1457
    }
1458
1459
    public function onBeforeDelete()
1460
    {
1461
        parent::onBeforeDelete();
1462
1463
        // If deleting this page, delete all its children.
1464
        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...
1465
            foreach ($children as $child) {
1466
                /** @var SiteTree $child */
1467
                $child->delete();
1468
            }
1469
        }
1470
    }
1471
1472
    public function onAfterDelete()
1473
    {
1474
        // Need to flush cache to avoid outdated versionnumber references
1475
        $this->flushCache();
1476
1477
        // Need to mark pages depending to this one as broken
1478
        $dependentPages = $this->DependentPages();
1479
        if ($dependentPages) {
1480
            foreach ($dependentPages as $page) {
1481
            // $page->write() calls syncLinkTracking, which does all the hard work for us.
1482
                $page->write();
1483
            }
1484
        }
1485
1486
        parent::onAfterDelete();
1487
    }
1488
1489
    public function flushCache($persistent = true)
1490
    {
1491
        parent::flushCache($persistent);
1492
        $this->_cache_statusFlags = null;
1493
    }
1494
1495
    public function validate()
1496
    {
1497
        $result = parent::validate();
1498
1499
        // Allowed children validation
1500
        $parent = $this->getParent();
1501
        if ($parent && $parent->exists()) {
1502
            // No need to check for subclasses or instanceof, as allowedChildren() already
1503
            // deconstructs any inheritance trees already.
1504
            $allowed = $parent->allowedChildren();
1505
            $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...
1506
                ? $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...
1507
                : $this;
1508
            if (!in_array($subject->ClassName, $allowed)) {
1509
                $result->addError(
1510
                    _t(
1511
                        'SilverStripe\\CMS\\Model\\SiteTree.PageTypeNotAllowed',
1512
                        'Page type "{type}" not allowed as child of this parent page',
1513
                        array('type' => $subject->i18n_singular_name())
1514
                    ),
1515
                    ValidationResult::TYPE_ERROR,
1516
                    'ALLOWED_CHILDREN'
1517
                );
1518
            }
1519
        }
1520
1521
        // "Can be root" validation
1522
        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...
1523
            $result->addError(
1524
                _t(
1525
                    'SilverStripe\\CMS\\Model\\SiteTree.PageTypNotAllowedOnRoot',
1526
                    'Page type "{type}" is not allowed on the root level',
1527
                    array('type' => $this->i18n_singular_name())
1528
                ),
1529
                ValidationResult::TYPE_ERROR,
1530
                'CAN_BE_ROOT'
1531
            );
1532
        }
1533
1534
        return $result;
1535
    }
1536
1537
    /**
1538
     * Returns true if this object has a URLSegment value that does not conflict with any other objects. This method
1539
     * checks for:
1540
     *  - A page with the same URLSegment that has a conflict
1541
     *  - Conflicts with actions on the parent page
1542
     *  - A conflict caused by a root page having the same URLSegment as a class name
1543
     *
1544
     * @return bool
1545
     */
1546
    public function validURLSegment()
1547
    {
1548
        if (self::config()->nested_urls && $parent = $this->Parent()) {
1549
            if ($controller = ModelAsController::controller_for($parent)) {
1550
                if ($controller instanceof Controller && $controller->hasAction($this->URLSegment)) {
1551
                    return false;
1552
                }
1553
            }
1554
        }
1555
1556
        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...
1557
            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...
1558
                return false;
1559
            }
1560
        }
1561
1562
        // Filters by url, id, and parent
1563
        $filter = array('"SiteTree"."URLSegment"' => $this->URLSegment);
1564
        if ($this->ID) {
1565
            $filter['"SiteTree"."ID" <> ?'] = $this->ID;
1566
        }
1567
        if (self::config()->nested_urls) {
1568
            $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...
1569
        }
1570
1571
        // If any of the extensions return `0` consider the segment invalid
1572
        $extensionResponses = array_filter(
1573
            (array)$this->extend('augmentValidURLSegment'),
1574
            function ($response) {
1575
                return !is_null($response);
1576
            }
1577
        );
1578
        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...
1579
            return min($extensionResponses);
1580
        }
1581
1582
        // Check existence
1583
        return !DataObject::get(self::class, $filter)->exists();
1584
    }
1585
1586
    /**
1587
     * Generate a URL segment based on the title provided.
1588
     *
1589
     * If {@link Extension}s wish to alter URL segment generation, they can do so by defining
1590
     * updateURLSegment(&$url, $title).  $url will be passed by reference and should be modified. $title will contain
1591
     * the title that was originally used as the source of this generated URL. This lets extensions either start from
1592
     * scratch, or incrementally modify the generated URL.
1593
     *
1594
     * @param string $title Page title
1595
     * @return string Generated url segment
1596
     */
1597
    public function generateURLSegment($title)
1598
    {
1599
        $filter = URLSegmentFilter::create();
1600
        $t = $filter->filter($title);
1601
1602
        // Fallback to generic page name if path is empty (= no valid, convertable characters)
1603
        if (!$t || $t == '-' || $t == '-1') {
1604
            $t = "page-$this->ID";
1605
        }
1606
1607
        // Hook for extensions
1608
        $this->extend('updateURLSegment', $t, $title);
1609
1610
        return $t;
1611
    }
1612
1613
    /**
1614
     * Gets the URL segment for the latest draft version of this page.
1615
     *
1616
     * @return string
1617
     */
1618
    public function getStageURLSegment()
1619
    {
1620
        $stageRecord = Versioned::get_one_by_stage(self::class, Versioned::DRAFT, array(
1621
            '"SiteTree"."ID"' => $this->ID
1622
        ));
1623
        return ($stageRecord) ? $stageRecord->URLSegment : null;
1624
    }
1625
1626
    /**
1627
     * Gets the URL segment for the currently published version of this page.
1628
     *
1629
     * @return string
1630
     */
1631
    public function getLiveURLSegment()
1632
    {
1633
        $liveRecord = Versioned::get_one_by_stage(self::class, Versioned::LIVE, array(
1634
            '"SiteTree"."ID"' => $this->ID
1635
        ));
1636
        return ($liveRecord) ? $liveRecord->URLSegment : null;
1637
    }
1638
1639
    /**
1640
     * Returns the pages that depend on this page. This includes virtual pages, pages that link to it, etc.
1641
     *
1642
     * @param bool $includeVirtuals Set to false to exlcude virtual pages.
1643
     * @return ArrayList
1644
     */
1645
    public function DependentPages($includeVirtuals = true)
1646
    {
1647
        if (class_exists('Subsite')) {
1648
            $origDisableSubsiteFilter = Subsite::$disable_subsite_filter;
1649
            Subsite::disable_subsite_filter(true);
1650
        }
1651
1652
        // Content links
1653
        $items = new ArrayList();
1654
1655
        // We merge all into a regular SS_List, because DataList doesn't support merge
1656
        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...
1657
            $linkList = new ArrayList();
1658
            foreach ($contentLinks as $item) {
1659
                $item->DependentLinkType = 'Content link';
1660
                $linkList->push($item);
1661
            }
1662
            $items->merge($linkList);
1663
        }
1664
1665
        // Virtual pages
1666
        if ($includeVirtuals) {
1667
            $virtuals = $this->VirtualPages();
1668
            if ($virtuals) {
1669
                $virtualList = new ArrayList();
1670
                foreach ($virtuals as $item) {
1671
                    $item->DependentLinkType = 'Virtual page';
1672
                    $virtualList->push($item);
1673
                }
1674
                $items->merge($virtualList);
1675
            }
1676
        }
1677
1678
        // Redirector pages
1679
        $redirectors = RedirectorPage::get()->where(array(
1680
            '"RedirectorPage"."RedirectionType"' => 'Internal',
1681
            '"RedirectorPage"."LinkToID"' => $this->ID
1682
        ));
1683
        if ($redirectors) {
1684
            $redirectorList = new ArrayList();
1685
            foreach ($redirectors as $item) {
1686
                $item->DependentLinkType = 'Redirector page';
1687
                $redirectorList->push($item);
1688
            }
1689
            $items->merge($redirectorList);
1690
        }
1691
1692
        if (class_exists('Subsite')) {
1693
            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...
1694
        }
1695
1696
        return $items;
1697
    }
1698
1699
    /**
1700
     * Return all virtual pages that link to this page.
1701
     *
1702
     * @return DataList
1703
     */
1704
    public function VirtualPages()
1705
    {
1706
        $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...
1707
1708
        // Disable subsite filter for these pages
1709
        if ($pages instanceof DataList) {
1710
            return $pages->setDataQueryParam('Subsite.filter', false);
1711
        } else {
1712
            return $pages;
1713
        }
1714
    }
1715
1716
    /**
1717
     * Returns a FieldList with which to create the main editing form.
1718
     *
1719
     * You can override this in your child classes to add extra fields - first get the parent fields using
1720
     * parent::getCMSFields(), then use addFieldToTab() on the FieldList.
1721
     *
1722
     * See {@link getSettingsFields()} for a different set of fields concerned with configuration aspects on the record,
1723
     * e.g. access control.
1724
     *
1725
     * @return FieldList The fields to be displayed in the CMS
1726
     */
1727
    public function getCMSFields()
1728
    {
1729
        // Status / message
1730
        // Create a status message for multiple parents
1731
        if ($this->ID && is_numeric($this->ID)) {
1732
            $linkedPages = $this->VirtualPages();
1733
1734
            $parentPageLinks = array();
1735
1736
            if ($linkedPages->count() > 0) {
1737
                /** @var VirtualPage $linkedPage */
1738
                foreach ($linkedPages as $linkedPage) {
1739
                    $parentPage = $linkedPage->Parent();
1740
                    if ($parentPage && $parentPage->exists()) {
1741
                        $link = Convert::raw2att($parentPage->CMSEditLink());
1742
                        $title = Convert::raw2xml($parentPage->Title);
1743
                    } else {
1744
                        $link = CMSPageEditController::singleton()->Link('show');
1745
                        $title = _t(__CLASS__.'.TOPLEVEL', 'Site Content (Top Level)');
1746
                    }
1747
                    $parentPageLinks[] = "<a class=\"cmsEditlink\" href=\"{$link}\">{$title}</a>";
1748
                }
1749
1750
                $lastParent = array_pop($parentPageLinks);
1751
                $parentList = "'$lastParent'";
1752
1753
                if (count($parentPageLinks)) {
1754
                    $parentList = "'" . implode("', '", $parentPageLinks) . "' and "
1755
                        . $parentList;
1756
                }
1757
1758
                $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...
1759
                    'SilverStripe\\CMS\\Model\\SiteTree.APPEARSVIRTUALPAGES',
1760
                    "This content also appears on the virtual pages in the {title} sections.",
1761
                    array('title' => $parentList)
1762
                );
1763
            }
1764
        }
1765
1766
        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...
1767
            $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...
1768
        }
1769
1770
        $dependentNote = '';
1771
        $dependentTable = new LiteralField('DependentNote', '<p></p>');
1772
1773
        // Create a table for showing pages linked to this one
1774
        $dependentPages = $this->DependentPages();
1775
        $dependentPagesCount = $dependentPages->count();
1776
        if ($dependentPagesCount) {
1777
            $dependentColumns = array(
1778
                'Title' => $this->fieldLabel('Title'),
1779
                'AbsoluteLink' => _t(__CLASS__.'.DependtPageColumnURL', 'URL'),
1780
                'DependentLinkType' => _t(__CLASS__.'.DependtPageColumnLinkType', 'Link type'),
1781
            );
1782
            if (class_exists('Subsite')) {
1783
                $dependentColumns['Subsite.Title'] = singleton('Subsite')->i18n_singular_name();
1784
            }
1785
1786
            $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>');
1787
            $dependentTable = GridField::create(
1788
                'DependentPages',
1789
                false,
1790
                $dependentPages
1791
            );
1792
            /** @var GridFieldDataColumns $dataColumns */
1793
            $dataColumns = $dependentTable->getConfig()->getComponentByType('SilverStripe\\Forms\\GridField\\GridFieldDataColumns');
1794
            $dataColumns
1795
                ->setDisplayFields($dependentColumns)
1796
                ->setFieldFormatting(array(
1797
                    'Title' => function ($value, &$item) {
1798
                        return sprintf(
1799
                            '<a href="admin/pages/edit/show/%d">%s</a>',
1800
                            (int)$item->ID,
1801
                            Convert::raw2xml($item->Title)
1802
                        );
1803
                    },
1804
                    '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...
1805
                        return sprintf(
1806
                            '<a href="%s" target="_blank">%s</a>',
1807
                            Convert::raw2xml($value),
1808
                            Convert::raw2xml($value)
1809
                        );
1810
                    }
1811
                ));
1812
        }
1813
1814
        $baseLink = Controller::join_links(
1815
            Director::absoluteBaseURL(),
1816
            (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...
1817
        );
1818
1819
        $urlsegment = SiteTreeURLSegmentField::create("URLSegment", $this->fieldLabel('URLSegment'))
1820
            ->setURLPrefix($baseLink)
1821
            ->setDefaultURL($this->generateURLSegment(_t(
1822
                'SilverStripe\\CMS\\Controllers\\CMSMain.NEWPAGE',
1823
                'New {pagetype}',
1824
                array('pagetype' => $this->i18n_singular_name())
1825
            )));
1826
        $helpText = (self::config()->nested_urls && $this->Children()->count())
0 ignored issues
show
Bug introduced by
The method Children() does not exist on SilverStripe\CMS\Model\SiteTree. Did you maybe mean duplicateWithChildren()?

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

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

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

This error could be the result of:

1. Missing dependencies

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

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

2. Missing use statement

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

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

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

Loading history...
2862
            $permissions->clearCache();
2863
        }
2864
    }
2865
}
2866