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

SiteTree::PreviewLink()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 11
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

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