Completed
Push — master ( 5c84f7...e7e184 )
by Ingo
11s
created

SiteTree::getSiteConfig()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 9
rs 9.6666
cc 2
eloc 5
nc 2
nop 0
1
<?php
2
3
namespace SilverStripe\CMS\Model;
4
5
use Page;
6
use SilverStripe\Admin\AddToCampaignHandler_FormAction;
7
use SilverStripe\Admin\CMSPreviewable;
8
use SilverStripe\CMS\Controllers\CMSPageEditController;
9
use SilverStripe\CMS\Controllers\ContentController;
10
use SilverStripe\CMS\Controllers\ModelAsController;
11
use SilverStripe\CMS\Controllers\RootURLController;
12
use SilverStripe\CMS\Forms\SiteTreeURLSegmentField;
13
use SilverStripe\Control\ContentNegotiator;
14
use SilverStripe\Control\Controller;
15
use SilverStripe\Control\Director;
16
use SilverStripe\Control\RequestHandler;
17
use SilverStripe\Core\ClassInfo;
18
use SilverStripe\Core\Config\Config;
19
use SilverStripe\Core\Convert;
20
use SilverStripe\Dev\Deprecation;
21
use SilverStripe\Forms\CheckboxField;
22
use SilverStripe\Forms\CompositeField;
23
use SilverStripe\Forms\DropdownField;
24
use SilverStripe\Forms\FieldGroup;
25
use SilverStripe\Forms\FieldList;
26
use SilverStripe\Forms\FormAction;
27
use SilverStripe\Forms\FormField;
28
use SilverStripe\Forms\GridField\GridField;
29
use SilverStripe\Forms\GridField\GridFieldDataColumns;
30
use SilverStripe\Forms\HTMLEditor\HTMLEditorField;
31
use SilverStripe\Forms\ListboxField;
32
use SilverStripe\Forms\LiteralField;
33
use SilverStripe\Forms\OptionsetField;
34
use SilverStripe\Forms\Tab;
35
use SilverStripe\Forms\TabSet;
36
use SilverStripe\Forms\TextareaField;
37
use SilverStripe\Forms\TextField;
38
use SilverStripe\Forms\ToggleCompositeField;
39
use SilverStripe\Forms\TreeDropdownField;
40
use SilverStripe\i18n\i18n;
41
use SilverStripe\i18n\i18nEntityProvider;
42
use SilverStripe\ORM\ArrayList;
43
use SilverStripe\ORM\DataList;
44
use SilverStripe\ORM\DataObject;
45
use SilverStripe\ORM\DB;
46
use SilverStripe\ORM\HiddenClass;
47
use SilverStripe\ORM\Hierarchy\Hierarchy;
48
use SilverStripe\ORM\ManyManyList;
49
use SilverStripe\ORM\ValidationResult;
50
use SilverStripe\ORM\Versioning\Versioned;
51
use SilverStripe\Security\Group;
52
use SilverStripe\Security\Member;
53
use SilverStripe\Security\Permission;
54
use SilverStripe\Security\PermissionProvider;
55
use SilverStripe\SiteConfig\SiteConfig;
56
use SilverStripe\View\ArrayData;
57
use SilverStripe\View\Parsers\ShortcodeParser;
58
use SilverStripe\View\Parsers\URLSegmentFilter;
59
use SilverStripe\View\SSViewer;
60
use Subsite;
61
62
/**
63
 * Basic data-object representing all pages within the site tree. All page types that live within the hierarchy should
64
 * inherit from this. In addition, it contains a number of static methods for querying the site tree and working with
65
 * draft and published states.
66
 *
67
 * <h2>URLs</h2>
68
 * A page is identified during request handling via its "URLSegment" database column. As pages can be nested, the full
69
 * path of a URL might contain multiple segments. Each segment is stored in its filtered representation (through
70
 * {@link URLSegmentFilter}). The full path is constructed via {@link Link()}, {@link RelativeLink()} and
71
 * {@link AbsoluteLink()}. You can allow these segments to contain multibyte characters through
72
 * {@link URLSegmentFilter::$default_allow_multibyte}.
73
 *
74
 * @property string URLSegment
75
 * @property string Title
76
 * @property string MenuTitle
77
 * @property string Content HTML content of the page.
78
 * @property string MetaDescription
79
 * @property string ExtraMeta
80
 * @property string ShowInMenus
81
 * @property string ShowInSearch
82
 * @property string Sort Integer value denoting the sort order.
83
 * @property string ReportClass
84
 * @property string CanViewType Type of restriction for viewing this object.
85
 * @property string CanEditType Type of restriction for editing this object.
86
 *
87
 * @method ManyManyList ViewerGroups() List of groups that can view this object.
88
 * @method ManyManyList EditorGroups() List of groups that can edit this object.
89
 * @method SiteTree Parent()
90
 *
91
 * @mixin Hierarchy
92
 * @mixin Versioned
93
 * @mixin SiteTreeLinkTracking
94
 */
95
class SiteTree extends DataObject implements PermissionProvider, i18nEntityProvider, CMSPreviewable
96
{
97
98
    /**
99
     * Indicates what kind of children this page type can have.
100
     * This can be an array of allowed child classes, or the string "none" -
101
     * indicating that this page type can't have children.
102
     * If a classname is prefixed by "*", such as "*Page", then only that
103
     * class is allowed - no subclasses. Otherwise, the class and all its
104
     * subclasses are allowed.
105
     * To control allowed children on root level (no parent), use {@link $can_be_root}.
106
     *
107
     * Note that this setting is cached when used in the CMS, use the "flush" query parameter to clear it.
108
     *
109
     * @config
110
     * @var array
111
     */
112
    private static $allowed_children = [
113
        self::class
114
    ];
115
116
    /**
117
     * The default child class for this page.
118
     * Note: Value might be cached, see {@link $allowed_chilren}.
119
     *
120
     * @config
121
     * @var string
122
     */
123
    private static $default_child = "Page";
124
125
    /**
126
     * Default value for SiteTree.ClassName enum
127
     * {@see DBClassName::getDefault}
128
     *
129
     * @config
130
     * @var string
131
     */
132
    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...
133
134
    /**
135
     * The default parent class for this page.
136
     * Note: Value might be cached, see {@link $allowed_chilren}.
137
     *
138
     * @config
139
     * @var string
140
     */
141
    private static $default_parent = null;
142
143
    /**
144
     * Controls whether a page can be in the root of the site tree.
145
     * Note: Value might be cached, see {@link $allowed_chilren}.
146
     *
147
     * @config
148
     * @var bool
149
     */
150
    private static $can_be_root = true;
151
152
    /**
153
     * List of permission codes a user can have to allow a user to create a page of this type.
154
     * Note: Value might be cached, see {@link $allowed_chilren}.
155
     *
156
     * @config
157
     * @var array
158
     */
159
    private static $need_permission = null;
160
161
    /**
162
     * If you extend a class, and don't want to be able to select the old class
163
     * in the cms, set this to the old class name. Eg, if you extended Product
164
     * to make ImprovedProduct, then you would set $hide_ancestor to Product.
165
     *
166
     * @config
167
     * @var string
168
     */
169
    private static $hide_ancestor = null;
170
171
    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...
172
        "URLSegment" => "Varchar(255)",
173
        "Title" => "Varchar(255)",
174
        "MenuTitle" => "Varchar(100)",
175
        "Content" => "HTMLText",
176
        "MetaDescription" => "Text",
177
        "ExtraMeta" => "HTMLFragment(['whitelist' => ['meta', 'link']])",
178
        "ShowInMenus" => "Boolean",
179
        "ShowInSearch" => "Boolean",
180
        "Sort" => "Int",
181
        "HasBrokenFile" => "Boolean",
182
        "HasBrokenLink" => "Boolean",
183
        "ReportClass" => "Varchar",
184
        "CanViewType" => "Enum('Anyone, LoggedInUsers, OnlyTheseUsers, Inherit', 'Inherit')",
185
        "CanEditType" => "Enum('LoggedInUsers, OnlyTheseUsers, Inherit', 'Inherit')",
186
    );
187
188
    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...
189
        "URLSegment" => true,
190
    );
191
192
    private static $many_many = array(
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
193
        "ViewerGroups" => Group::class,
194
        "EditorGroups" => Group::class,
195
    );
196
197
    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...
198
        "VirtualPages" => "SilverStripe\\CMS\\Model\\VirtualPage.CopyContentFrom"
199
    );
200
201
    private static $owned_by = array(
202
        "VirtualPages"
203
    );
204
205
    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...
206
        "Breadcrumbs" => "HTMLFragment",
207
        "LastEdited" => "Datetime",
208
        "Created" => "Datetime",
209
        'Link' => 'Text',
210
        'RelativeLink' => 'Text',
211
        'AbsoluteLink' => 'Text',
212
        'CMSEditLink' => 'Text',
213
        'TreeTitle' => 'HTMLFragment',
214
        'MetaTags' => 'HTMLFragment',
215
    );
216
217
    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...
218
        "ShowInMenus" => 1,
219
        "ShowInSearch" => 1,
220
        "CanViewType" => "Inherit",
221
        "CanEditType" => "Inherit"
222
    );
223
224
    private static $table_name = 'SiteTree';
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
225
226
    private static $versioning = array(
227
        "Stage",  "Live"
228
    );
229
230
    private static $default_sort = "\"Sort\"";
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
231
232
    /**
233
     * If this is false, the class cannot be created in the CMS by regular content authors, only by ADMINs.
234
     * @var boolean
235
     * @config
236
     */
237
    private static $can_create = true;
238
239
    /**
240
     * Icon to use in the CMS page tree. This should be the full filename, relative to the webroot.
241
     * Also supports custom CSS rule contents (applied to the correct selector for the tree UI implementation).
242
     *
243
     * @see CMSMain::generateTreeStylingCSS()
244
     * @config
245
     * @var string
246
     */
247
    private static $icon = null;
248
249
    private static $extensions = [
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
250
        Hierarchy::class,
251
        Versioned::class,
252
        SiteTreeLinkTracking::class,
253
    ];
254
255
    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...
256
        'Title',
257
        'Content',
258
    );
259
260
    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...
261
        'URLSegment' => 'URL'
262
    );
263
264
    /**
265
     * @config
266
     */
267
    private static $nested_urls = true;
268
269
    /**
270
     * @config
271
    */
272
    private static $create_default_pages = true;
273
274
    /**
275
     * This controls whether of not extendCMSFields() is called by getCMSFields.
276
     */
277
    private static $runCMSFieldsExtensions = true;
278
279
    /**
280
     * Cache for canView/Edit/Publish/Delete permissions.
281
     * Keyed by permission type (e.g. 'edit'), with an array
282
     * of IDs mapped to their boolean permission ability (true=allow, false=deny).
283
     * See {@link batch_permission_check()} for details.
284
     */
285
    private static $cache_permissions = array();
286
287
    /**
288
     * @config
289
     * @var boolean
290
     */
291
    private static $enforce_strict_hierarchy = true;
292
293
    /**
294
     * The value used for the meta generator tag. Leave blank to omit the tag.
295
     *
296
     * @config
297
     * @var string
298
     */
299
    private static $meta_generator = 'SilverStripe - http://silverstripe.org';
300
301
    protected $_cache_statusFlags = null;
302
303
    /**
304
     * Plural form for SiteTree / Page classes. Not inherited by subclasses.
305
     *
306
     * @config
307
     * @var string
308
     */
309
    private static $base_plural_name = 'Pages';
310
311
    /**
312
     * Plural form for SiteTree / Page classes. Not inherited by subclasses.
313
     *
314
     * @config
315
     * @var string
316
     */
317
    private static $base_singular_name = 'Page';
318
319
    /**
320
     * Description of the class functionality, typically shown to a user
321
     * when selecting which page type to create. Translated through {@link provideI18nEntities()}.
322
     *
323
     * @see SiteTree::description()
324
     * @see SiteTree::i18n_description()
325
     *
326
     * @config
327
     * @var string
328
     */
329
    private static $description = null;
330
331
    /**
332
     * Description for Page and SiteTree classes, but not inherited by subclasses.
333
     * override SiteTree::$description in subclasses instead.
334
     *
335
     * @see SiteTree::description()
336
     * @see SiteTree::i18n_description()
337
     *
338
     * @config
339
     * @var string
340
     */
341
    private static $base_description = 'Generic content page';
342
343
    /**
344
     * Fetches the {@link SiteTree} object that maps to a link.
345
     *
346
     * If you have enabled {@link SiteTree::config()->nested_urls} on this site, then you can use a nested link such as
347
     * "about-us/staff/", and this function will traverse down the URL chain and grab the appropriate link.
348
     *
349
     * Note that if no model can be found, this method will fall over to a extended alternateGetByLink method provided
350
     * by a extension attached to {@link SiteTree}
351
     *
352
     * @param string $link  The link of the page to search for
353
     * @param bool   $cache True (default) to use caching, false to force a fresh search from the database
354
     * @return SiteTree
355
     */
356
    public static function get_by_link($link, $cache = true)
357
    {
358
        if (trim($link, '/')) {
359
            $link = trim(Director::makeRelative($link), '/');
360
        } else {
361
            $link = RootURLController::get_homepage_link();
362
        }
363
364
        $parts = preg_split('|/+|', $link);
365
366
        // Grab the initial root level page to traverse down from.
367
        $URLSegment = array_shift($parts);
368
        $conditions = array('"SiteTree"."URLSegment"' => rawurlencode($URLSegment));
369
        if (self::config()->nested_urls) {
370
            $conditions[] = array('"SiteTree"."ParentID"' => 0);
371
        }
372
        /** @var SiteTree $sitetree */
373
        $sitetree = DataObject::get_one(self::class, $conditions, $cache);
374
375
        /// Fall back on a unique URLSegment for b/c.
376
        if (!$sitetree
377
            && self::config()->nested_urls
378
            && $sitetree = DataObject::get_one(self::class, array(
379
                '"SiteTree"."URLSegment"' => $URLSegment
380
            ), $cache)
381
        ) {
382
            return $sitetree;
383
        }
384
385
        // Attempt to grab an alternative page from extensions.
386
        if (!$sitetree) {
387
            $parentID = self::config()->nested_urls ? 0 : null;
388
389 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...
390
                foreach ($alternatives as $alternative) {
391
                    if ($alternative) {
392
                        $sitetree = $alternative;
393
                    }
394
                }
395
            }
396
397
            if (!$sitetree) {
398
                return null;
399
            }
400
        }
401
402
        // Check if we have any more URL parts to parse.
403
        if (!self::config()->nested_urls || !count($parts)) {
404
            return $sitetree;
405
        }
406
407
        // Traverse down the remaining URL segments and grab the relevant SiteTree objects.
408
        foreach ($parts as $segment) {
409
            $next = DataObject::get_one(
410
                self::class,
411
                array(
412
                    '"SiteTree"."URLSegment"' => $segment,
413
                    '"SiteTree"."ParentID"' => $sitetree->ID
414
                ),
415
                $cache
416
            );
417
418
            if (!$next) {
419
                $parentID = (int) $sitetree->ID;
420
421 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...
422
                    foreach ($alternatives as $alternative) {
423
                        if ($alternative) {
424
                            $next = $alternative;
425
                        }
426
                    }
427
                }
428
429
                if (!$next) {
430
                    return null;
431
                }
432
            }
433
434
            $sitetree->destroy();
435
            $sitetree = $next;
436
        }
437
438
        return $sitetree;
439
    }
440
441
    /**
442
     * Return a subclass map of SiteTree that shouldn't be hidden through {@link SiteTree::$hide_ancestor}
443
     *
444
     * @return array
445
     */
446
    public static function page_type_classes()
447
    {
448
        $classes = ClassInfo::getValidSubClasses();
449
450
        $baseClassIndex = array_search(self::class, $classes);
451
        if ($baseClassIndex !== false) {
452
            unset($classes[$baseClassIndex]);
453
        }
454
455
        $kill_ancestors = array();
456
457
        // figure out if there are any classes we don't want to appear
458
        foreach ($classes as $class) {
459
            $instance = singleton($class);
460
461
            // do any of the progeny want to hide an ancestor?
462
            if ($ancestor_to_hide = $instance->stat('hide_ancestor')) {
463
                // note for killing later
464
                $kill_ancestors[] = $ancestor_to_hide;
465
            }
466
        }
467
468
        // If any of the descendents don't want any of the elders to show up, cruelly render the elders surplus to
469
        // requirements
470
        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...
471
            $kill_ancestors = array_unique($kill_ancestors);
472
            foreach ($kill_ancestors as $mark) {
473
                // unset from $classes
474
                $idx = array_search($mark, $classes, true);
475
                if ($idx !== false) {
476
                    unset($classes[$idx]);
477
                }
478
            }
479
        }
480
481
        return $classes;
482
    }
483
484
    /**
485
     * Replace a "[sitetree_link id=n]" shortcode with a link to the page with the corresponding ID.
486
     *
487
     * @param array      $arguments
488
     * @param string     $content
489
     * @param ShortcodeParser $parser
490
     * @return string
491
     */
492
    public static function link_shortcode_handler($arguments, $content = null, $parser = null)
493
    {
494
        if (!isset($arguments['id']) || !is_numeric($arguments['id'])) {
495
            return null;
496
        }
497
498
        /** @var SiteTree $page */
499
        if (!($page = DataObject::get_by_id(self::class, $arguments['id']))         // Get the current page by ID.
500
            && !($page = Versioned::get_latest_version(self::class, $arguments['id'])) // Attempt link to old version.
501
        ) {
502
             return null; // There were no suitable matches at all.
503
        }
504
505
        /** @var SiteTree $page */
506
        $link = Convert::raw2att($page->Link());
507
508
        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...
509
            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...
510
        } else {
511
            return $link;
512
        }
513
    }
514
515
    /**
516
     * Return the link for this {@link SiteTree} object, with the {@link Director::baseURL()} included.
517
     *
518
     * @param string $action Optional controller action (method).
519
     *                       Note: URI encoding of this parameter is applied automatically through template casting,
520
     *                       don't encode the passed parameter. Please use {@link Controller::join_links()} instead to
521
     *                       append GET parameters.
522
     * @return string
523
     */
524
    public function Link($action = null)
525
    {
526
        return Controller::join_links(Director::baseURL(), $this->RelativeLink($action));
527
    }
528
529
    /**
530
     * Get the absolute URL for this page, including protocol and host.
531
     *
532
     * @param string $action See {@link Link()}
533
     * @return string
534
     */
535
    public function AbsoluteLink($action = null)
536
    {
537
        if ($this->hasMethod('alternateAbsoluteLink')) {
538
            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...
539
        } else {
540
            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 540 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...
541
        }
542
    }
543
544
    /**
545
     * Base link used for previewing. Defaults to absolute URL, in order to account for domain changes, e.g. on multi
546
     * site setups. Does not contain hints about the stage, see {@link SilverStripeNavigator} for details.
547
     *
548
     * @param string $action See {@link Link()}
549
     * @return string
550
     */
551
    public function PreviewLink($action = null)
552
    {
553
        if ($this->hasMethod('alternatePreviewLink')) {
554
            Deprecation::notice('5.0', 'Use updatePreviewLink or override PreviewLink method');
555
            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...
556
        }
557
558
        $link = $this->AbsoluteLink($action);
559
        $this->extend('updatePreviewLink', $link, $action);
560
        return $link;
561
    }
562
563
    public function getMimeType()
564
    {
565
        return 'text/html';
566
    }
567
568
    /**
569
     * Return the link for this {@link SiteTree} object relative to the SilverStripe root.
570
     *
571
     * By default, if this page is the current home page, and there is no action specified then this will return a link
572
     * to the root of the site. However, if you set the $action parameter to TRUE then the link will not be rewritten
573
     * and returned in its full form.
574
     *
575
     * @uses RootURLController::get_homepage_link()
576
     *
577
     * @param string $action See {@link Link()}
578
     * @return string
579
     */
580
    public function RelativeLink($action = null)
581
    {
582
        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...
583
            $parent = $this->Parent();
584
            // If page is removed select parent from version history (for archive page view)
585
            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...
586
                $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...
587
            }
588
            $base = $parent->RelativeLink($this->URLSegment);
589
        } 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...
590
            // Unset base for root-level homepages.
591
            // Note: Homepages with action parameters (or $action === true)
592
            // need to retain their URLSegment.
593
            $base = null;
594
        } else {
595
            $base = $this->URLSegment;
596
        }
597
598
        $this->extend('updateRelativeLink', $base, $action);
599
600
        // Legacy support: If $action === true, retain URLSegment for homepages,
601
        // but don't append any action
602
        if ($action === true) {
603
            $action = null;
604
        }
605
606
        return Controller::join_links($base, '/', $action);
607
    }
608
609
    /**
610
     * Get the absolute URL for this page on the Live site.
611
     *
612
     * @param bool $includeStageEqualsLive Whether to append the URL with ?stage=Live to force Live mode
613
     * @return string
614
     */
615
    public function getAbsoluteLiveLink($includeStageEqualsLive = true)
616
    {
617
        $oldReadingMode = Versioned::get_reading_mode();
618
        Versioned::set_stage(Versioned::LIVE);
619
        /** @var SiteTree $live */
620
        $live = Versioned::get_one_by_stage(self::class, Versioned::LIVE, array(
0 ignored issues
show
Documentation introduced by
array('"SiteTree"."ID"' => $this->ID) is of type array<string,integer|str...D\"":"integer|string"}>, but the function expects a string.

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
621
            '"SiteTree"."ID"' => $this->ID
622
        ));
623
        if ($live) {
624
            $link = $live->AbsoluteLink();
625
            if ($includeStageEqualsLive) {
626
                $link = Controller::join_links($link, '?stage=Live');
627
            }
628
        } else {
629
            $link = null;
630
        }
631
632
        Versioned::set_reading_mode($oldReadingMode);
633
        return $link;
634
    }
635
636
    /**
637
     * Generates a link to edit this page in the CMS.
638
     *
639
     * @return string
640
     */
641
    public function CMSEditLink()
642
    {
643
        $link = Controller::join_links(
644
            CMSPageEditController::singleton()->Link('show'),
645
            $this->ID
646
        );
647
        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 647 which is incompatible with the return type declared by the interface SilverStripe\Admin\CMSPreviewable::CMSEditLink of type string. It seems like you forgot to handle an error condition.
Loading history...
648
    }
649
650
651
    /**
652
     * Return a CSS identifier generated from this page's link.
653
     *
654
     * @return string The URL segment
655
     */
656
    public function ElementName()
657
    {
658
        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...
659
    }
660
661
    /**
662
     * Returns true if this is the currently active page being used to handle this request.
663
     *
664
     * @return bool
665
     */
666
    public function isCurrent()
667
    {
668
        $currentPage = Director::get_current_page();
669
        if ($currentPage instanceof ContentController) {
670
            $currentPage = $currentPage->data();
671
        }
672
        if ($currentPage instanceof SiteTree) {
673
            return $currentPage === $this || $currentPage->ID === $this->ID;
674
        }
675
        return false;
676
    }
677
678
    /**
679
     * Check if this page is in the currently active section (e.g. it is either current or one of its children is
680
     * currently being viewed).
681
     *
682
     * @return bool
683
     */
684
    public function isSection()
685
    {
686
        return $this->isCurrent() || (
687
            Director::get_current_page() instanceof SiteTree && in_array($this->ID, Director::get_current_page()->getAncestors()->column())
688
        );
689
    }
690
691
    /**
692
     * Check if the parent of this page has been removed (or made otherwise unavailable), and is still referenced by
693
     * this child. Any such orphaned page may still require access via the CMS, but should not be shown as accessible
694
     * to external users.
695
     *
696
     * @return bool
697
     */
698
    public function isOrphaned()
699
    {
700
        // Always false for root pages
701
        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...
702
            return false;
703
        }
704
705
        // Parent must exist and not be an orphan itself
706
        $parent = $this->Parent();
707
        return !$parent || !$parent->exists() || $parent->isOrphaned();
708
    }
709
710
    /**
711
     * Return "link" or "current" depending on if this is the {@link SiteTree::isCurrent()} current page.
712
     *
713
     * @return string
714
     */
715
    public function LinkOrCurrent()
716
    {
717
        return $this->isCurrent() ? 'current' : 'link';
718
    }
719
720
    /**
721
     * Return "link" or "section" depending on if this is the {@link SiteTree::isSeciton()} current section.
722
     *
723
     * @return string
724
     */
725
    public function LinkOrSection()
726
    {
727
        return $this->isSection() ? 'section' : 'link';
728
    }
729
730
    /**
731
     * Return "link", "current" or "section" depending on if this page is the current page, or not on the current page
732
     * but in the current section.
733
     *
734
     * @return string
735
     */
736
    public function LinkingMode()
737
    {
738
        if ($this->isCurrent()) {
739
            return 'current';
740
        } elseif ($this->isSection()) {
741
            return 'section';
742
        } else {
743
            return 'link';
744
        }
745
    }
746
747
    /**
748
     * Check if this page is in the given current section.
749
     *
750
     * @param string $sectionName Name of the section to check
751
     * @return bool True if we are in the given section
752
     */
753
    public function InSection($sectionName)
754
    {
755
        $page = Director::get_current_page();
756
        while ($page && $page->exists()) {
757
            if ($sectionName == $page->URLSegment) {
758
                return true;
759
            }
760
            $page = $page->Parent();
761
        }
762
        return false;
763
    }
764
765
    /**
766
     * Reset Sort on duped page
767
     *
768
     * @param SiteTree $original
769
     * @param bool $doWrite
770
     */
771
    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...
772
    {
773
        $this->Sort = 0;
774
    }
775
776
    /**
777
     * Duplicates each child of this node recursively and returns the top-level duplicate node.
778
     *
779
     * @return static The duplicated object
780
     */
781
    public function duplicateWithChildren()
782
    {
783
        /** @var SiteTree $clone */
784
        $clone = $this->duplicate();
785
        $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...
786
787
        if ($children) {
788
            /** @var SiteTree $child */
789
            $sort = 0;
790
            foreach ($children as $child) {
791
                $childClone = $child->duplicateWithChildren();
792
                $childClone->ParentID = $clone->ID;
793
                //retain sort order by manually setting sort values
794
                $childClone->Sort = ++$sort;
795
                $childClone->write();
796
            }
797
        }
798
799
        return $clone;
800
    }
801
802
    /**
803
     * Duplicate this node and its children as a child of the node with the given ID
804
     *
805
     * @param int $id ID of the new node's new parent
806
     */
807
    public function duplicateAsChild($id)
808
    {
809
        /** @var SiteTree $newSiteTree */
810
        $newSiteTree = $this->duplicate();
811
        $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...
812
        $newSiteTree->Sort = 0;
813
        $newSiteTree->write();
814
    }
815
816
    /**
817
     * Return a breadcrumb trail to this page. Excludes "hidden" pages (with ShowInMenus=0) by default.
818
     *
819
     * @param int $maxDepth The maximum depth to traverse.
820
     * @param boolean $unlinked Whether to link page titles.
821
     * @param boolean|string $stopAtPageType ClassName of a page to stop the upwards traversal.
822
     * @param boolean $showHidden Include pages marked with the attribute ShowInMenus = 0
823
     * @return string The breadcrumb trail.
824
     */
825
    public function Breadcrumbs($maxDepth = 20, $unlinked = false, $stopAtPageType = false, $showHidden = false)
826
    {
827
        $pages = $this->getBreadcrumbItems($maxDepth, $stopAtPageType, $showHidden);
828
        $template = new SSViewer('BreadcrumbsTemplate');
829
        return $template->process($this->customise(new ArrayData(array(
830
            "Pages" => $pages,
831
            "Unlinked" => $unlinked
832
        ))));
833
    }
834
835
836
    /**
837
     * Returns a list of breadcrumbs for the current page.
838
     *
839
     * @param int $maxDepth The maximum depth to traverse.
840
     * @param boolean|string $stopAtPageType ClassName of a page to stop the upwards traversal.
841
     * @param boolean $showHidden Include pages marked with the attribute ShowInMenus = 0
842
     *
843
     * @return ArrayList
844
    */
845
    public function getBreadcrumbItems($maxDepth = 20, $stopAtPageType = false, $showHidden = false)
846
    {
847
        $page = $this;
848
        $pages = array();
849
850
        while ($page
851
            && $page->exists()
852
            && (!$maxDepth || count($pages) < $maxDepth)
853
            && (!$stopAtPageType || $page->ClassName != $stopAtPageType)
854
        ) {
855
            if ($showHidden || $page->ShowInMenus || ($page->ID == $this->ID)) {
856
                $pages[] = $page;
857
            }
858
859
            $page = $page->Parent();
860
        }
861
862
        return new ArrayList(array_reverse($pages));
863
    }
864
865
866
    /**
867
     * Make this page a child of another page.
868
     *
869
     * If the parent page does not exist, resolve it to a valid ID before updating this page's reference.
870
     *
871
     * @param SiteTree|int $item Either the parent object, or the parent ID
872
     */
873
    public function setParent($item)
874
    {
875
        if (is_object($item)) {
876
            if (!$item->exists()) {
877
                $item->write();
878
            }
879
            $this->setField("ParentID", $item->ID);
880
        } else {
881
            $this->setField("ParentID", $item);
882
        }
883
    }
884
885
    /**
886
     * Get the parent of this page.
887
     *
888
     * @return SiteTree Parent of this page
889
     */
890
    public function getParent()
891
    {
892
        if ($parentID = $this->getField("ParentID")) {
893
            return DataObject::get_by_id(self::class, $parentID);
894
        }
895
        return null;
896
    }
897
898
    /**
899
     * Return a string of the form "parent - page" or "grandparent - parent - page" using page titles
900
     *
901
     * @param int $level The maximum amount of levels to traverse.
902
     * @param string $separator Seperating string
903
     * @return string The resulting string
904
     */
905
    public function NestedTitle($level = 2, $separator = " - ")
906
    {
907
        $item = $this;
908
        $parts = [];
909
        while ($item && $level > 0) {
910
            $parts[] = $item->Title;
911
            $item = $item->getParent();
912
            $level--;
913
        }
914
        return implode($separator, array_reverse($parts));
915
    }
916
917
    /**
918
     * This function should return true if the current user can execute this action. It can be overloaded to customise
919
     * the security model for an application.
920
     *
921
     * Slightly altered from parent behaviour in {@link DataObject->can()}:
922
     * - Checks for existence of a method named "can<$perm>()" on the object
923
     * - Calls decorators and only returns for FALSE "vetoes"
924
     * - Falls back to {@link Permission::check()}
925
     * - Does NOT check for many-many relations named "Can<$perm>"
926
     *
927
     * @uses DataObjectDecorator->can()
928
     *
929
     * @param string $perm The permission to be checked, such as 'View'
930
     * @param Member $member The member whose permissions need checking. Defaults to the currently logged in user.
931
     * @param array $context Context argument for canCreate()
932
     * @return bool True if the the member is allowed to do the given action
933
     */
934
    public function can($perm, $member = null, $context = array())
935
    {
936 View Code Duplication
        if (!$member || !($member instanceof Member) || is_numeric($member)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
937
            $member = Member::currentUserID();
938
        }
939
940
        if ($member && Permission::checkMember($member, "ADMIN")) {
941
            return true;
942
        }
943
944
        if (is_string($perm) && method_exists($this, 'can' . ucfirst($perm))) {
945
            $method = 'can' . ucfirst($perm);
946
            return $this->$method($member);
947
        }
948
949
        $results = $this->extend('can', $member);
950
        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...
951
            if (!min($results)) {
952
                return false;
953
            }
954
        }
955
956
        return ($member && Permission::checkMember($member, $perm));
957
    }
958
959
    /**
960
     * This function should return true if the current user can add children to this page. It can be overloaded to
961
     * customise the security model for an application.
962
     *
963
     * Denies permission if any of the following conditions is true:
964
     * - alternateCanAddChildren() on a extension returns false
965
     * - canEdit() is not granted
966
     * - There are no classes defined in {@link $allowed_children}
967
     *
968
     * @uses SiteTreeExtension->canAddChildren()
969
     * @uses canEdit()
970
     * @uses $allowed_children
971
     *
972
     * @param Member|int $member
973
     * @return bool True if the current user can add children
974
     */
975
    public function canAddChildren($member = null)
976
    {
977
        // Disable adding children to archived pages
978
        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...
979
            return false;
980
        }
981
982 View Code Duplication
        if (!$member || !($member instanceof Member) || is_numeric($member)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
983
            $member = Member::currentUserID();
984
        }
985
986
        // Standard mechanism for accepting permission changes from extensions
987
        $extended = $this->extendedCan('canAddChildren', $member);
988
        if ($extended !== null) {
989
            return $extended;
990
        }
991
992
        // Default permissions
993
        if ($member && Permission::checkMember($member, "ADMIN")) {
994
            return true;
995
        }
996
997
        return $this->canEdit($member) && $this->stat('allowed_children') != 'none';
998
    }
999
1000
    /**
1001
     * This function should return true if the current user can view this page. It can be overloaded to customise the
1002
     * security model for an application.
1003
     *
1004
     * Denies permission if any of the following conditions is true:
1005
     * - canView() on any extension returns false
1006
     * - "CanViewType" directive is set to "Inherit" and any parent page return false for canView()
1007
     * - "CanViewType" directive is set to "LoggedInUsers" and no user is logged in
1008
     * - "CanViewType" directive is set to "OnlyTheseUsers" and user is not in the given groups
1009
     *
1010
     * @uses DataExtension->canView()
1011
     * @uses ViewerGroups()
1012
     *
1013
     * @param Member|int $member
1014
     * @return bool True if the current user can view this page
1015
     */
1016
    public function canView($member = null)
1017
    {
1018 View Code Duplication
        if (!$member || !($member instanceof Member) || is_numeric($member)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
1019
            $member = Member::currentUserID();
1020
        }
1021
1022
        // Standard mechanism for accepting permission changes from extensions
1023
        $extended = $this->extendedCan('canView', $member);
1024
        if ($extended !== null) {
1025
            return $extended;
1026
        }
1027
1028
        // admin override
1029
        if ($member && Permission::checkMember($member, array("ADMIN", "SITETREE_VIEW_ALL"))) {
1030
            return true;
1031
        }
1032
1033
        // Orphaned pages (in the current stage) are unavailable, except for admins via the CMS
1034
        if ($this->isOrphaned()) {
1035
            return false;
1036
        }
1037
1038
        // check for empty spec
1039
        if (!$this->CanViewType || $this->CanViewType == 'Anyone') {
1040
            return true;
1041
        }
1042
1043
        // check for inherit
1044
        if ($this->CanViewType == 'Inherit') {
1045
            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...
1046
                return $this->Parent()->canView($member);
1047
            } else {
1048
                return $this->getSiteConfig()->canViewPages($member);
1049
            }
1050
        }
1051
1052
        // check for any logged-in users
1053
        if ($this->CanViewType == 'LoggedInUsers' && $member) {
1054
            return true;
1055
        }
1056
1057
        // check for specific groups
1058
        if ($member && is_numeric($member)) {
1059
            $member = DataObject::get_by_id('SilverStripe\\Security\\Member', $member);
1060
        }
1061
        if ($this->CanViewType == 'OnlyTheseUsers'
1062
            && $member
1063
            && $member->inGroups($this->ViewerGroups())
1064
        ) {
1065
            return true;
1066
        }
1067
1068
        return false;
1069
    }
1070
1071
    /**
1072
     * Check if this page can be published
1073
     *
1074
     * @param Member $member
1075
     * @return bool
1076
     */
1077
    public function canPublish($member = null)
1078
    {
1079
        if (!$member) {
1080
            $member = Member::currentUser();
1081
        }
1082
1083
        // Check extension
1084
        $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...
1085
        if ($extended !== null) {
1086
            return $extended;
1087
        }
1088
1089
        if (Permission::checkMember($member, "ADMIN")) {
1090
            return true;
1091
        }
1092
1093
        // Default to relying on edit permission
1094
        return $this->canEdit($member);
1095
    }
1096
1097
    /**
1098
     * This function should return true if the current user can delete this page. It can be overloaded to customise the
1099
     * security model for an application.
1100
     *
1101
     * Denies permission if any of the following conditions is true:
1102
     * - canDelete() returns false on any extension
1103
     * - canEdit() returns false
1104
     * - any descendant page returns false for canDelete()
1105
     *
1106
     * @uses canDelete()
1107
     * @uses SiteTreeExtension->canDelete()
1108
     * @uses canEdit()
1109
     *
1110
     * @param Member $member
1111
     * @return bool True if the current user can delete this page
1112
     */
1113
    public function canDelete($member = null)
1114
    {
1115 View Code Duplication
        if ($member instanceof Member) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
1116
            $memberID = $member->ID;
1117
        } elseif (is_numeric($member)) {
1118
            $memberID = $member;
1119
        } else {
1120
            $memberID = Member::currentUserID();
1121
        }
1122
1123
        // Standard mechanism for accepting permission changes from extensions
1124
        $extended = $this->extendedCan('canDelete', $memberID);
1125
        if ($extended !== null) {
1126
            return $extended;
1127
        }
1128
1129
        // Default permission check
1130
        if ($memberID && Permission::checkMember($memberID, array("ADMIN", "SITETREE_EDIT_ALL"))) {
1131
            return true;
1132
        }
1133
1134
        // Regular canEdit logic is handled by can_edit_multiple
1135
        $results = self::can_delete_multiple(array($this->ID), $memberID);
1136
1137
        // If this page no longer exists in stage/live results won't contain the page.
1138
        // Fail-over to false
1139
        return isset($results[$this->ID]) ? $results[$this->ID] : false;
1140
    }
1141
1142
    /**
1143
     * This function should return true if the current user can create new pages of this class, regardless of class. It
1144
     * can be overloaded to customise the security model for an application.
1145
     *
1146
     * By default, permission to create at the root level is based on the SiteConfig configuration, and permission to
1147
     * create beneath a parent is based on the ability to edit that parent page.
1148
     *
1149
     * Use {@link canAddChildren()} to control behaviour of creating children under this page.
1150
     *
1151
     * @uses $can_create
1152
     * @uses DataExtension->canCreate()
1153
     *
1154
     * @param Member $member
1155
     * @param array $context Optional array which may contain array('Parent' => $parentObj)
1156
     *                       If a parent page is known, it will be checked for validity.
1157
     *                       If omitted, it will be assumed this is to be created as a top level page.
1158
     * @return bool True if the current user can create pages on this class.
1159
     */
1160
    public function canCreate($member = null, $context = array())
1161
    {
1162 View Code Duplication
        if (!$member || !(is_a($member, 'SilverStripe\\Security\\Member')) || is_numeric($member)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
1163
            $member = Member::currentUserID();
1164
        }
1165
1166
        // Check parent (custom canCreate option for SiteTree)
1167
        // Block children not allowed for this parent type
1168
        $parent = isset($context['Parent']) ? $context['Parent'] : null;
1169
        if ($parent && !in_array(static::class, $parent->allowedChildren())) {
1170
            return false;
1171
        }
1172
1173
        // Standard mechanism for accepting permission changes from extensions
1174
        $extended = $this->extendedCan(__FUNCTION__, $member, $context);
1175
        if ($extended !== null) {
1176
            return $extended;
1177
        }
1178
1179
        // Check permission
1180
        if ($member && Permission::checkMember($member, "ADMIN")) {
1181
            return true;
1182
        }
1183
1184
        // Fall over to inherited permissions
1185
        if ($parent && $parent->exists()) {
1186
            return $parent->canAddChildren($member);
1187
        } else {
1188
            // This doesn't necessarily mean we are creating a root page, but that
1189
            // we don't know if there is a parent, so default to this permission
1190
            return SiteConfig::current_site_config()->canCreateTopLevel($member);
1191
        }
1192
    }
1193
1194
    /**
1195
     * This function should return true if the current user can edit this page. It can be overloaded to customise the
1196
     * security model for an application.
1197
     *
1198
     * Denies permission if any of the following conditions is true:
1199
     * - canEdit() on any extension returns false
1200
     * - canView() return false
1201
     * - "CanEditType" directive is set to "Inherit" and any parent page return false for canEdit()
1202
     * - "CanEditType" directive is set to "LoggedInUsers" and no user is logged in or doesn't have the
1203
     *   CMS_Access_CMSMAIN permission code
1204
     * - "CanEditType" directive is set to "OnlyTheseUsers" and user is not in the given groups
1205
     *
1206
     * @uses canView()
1207
     * @uses EditorGroups()
1208
     * @uses DataExtension->canEdit()
1209
     *
1210
     * @param Member $member Set to false if you want to explicitly test permissions without a valid user (useful for
1211
     *                       unit tests)
1212
     * @return bool True if the current user can edit this page
1213
     */
1214
    public function canEdit($member = null)
1215
    {
1216 View Code Duplication
        if ($member instanceof Member) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
1217
            $memberID = $member->ID;
1218
        } elseif (is_numeric($member)) {
1219
            $memberID = $member;
1220
        } else {
1221
            $memberID = Member::currentUserID();
1222
        }
1223
1224
        // Standard mechanism for accepting permission changes from extensions
1225
        $extended = $this->extendedCan('canEdit', $memberID);
1226
        if ($extended !== null) {
1227
            return $extended;
1228
        }
1229
1230
        // Default permissions
1231
        if ($memberID && Permission::checkMember($memberID, array("ADMIN", "SITETREE_EDIT_ALL"))) {
1232
            return true;
1233
        }
1234
1235
        if ($this->ID) {
1236
            // Regular canEdit logic is handled by can_edit_multiple
1237
            $results = self::can_edit_multiple(array($this->ID), $memberID);
1238
1239
            // If this page no longer exists in stage/live results won't contain the page.
1240
            // Fail-over to false
1241
            return isset($results[$this->ID]) ? $results[$this->ID] : false;
1242
1243
        // Default for unsaved pages
1244
        } else {
1245
            return $this->getSiteConfig()->canEditPages($member);
1246
        }
1247
    }
1248
1249
    /**
1250
     * Stub method to get the site config, unless the current class can provide an alternate.
1251
     *
1252
     * @return SiteConfig
1253
     */
1254
    public function getSiteConfig()
1255
    {
1256
        $configs = $this->invokeWithExtensions('alternateSiteConfig');
1257
        foreach (array_filter($configs) as $config) {
1258
            return $config;
1259
        }
1260
1261
        return SiteConfig::current_site_config();
1262
    }
1263
1264
    /**
1265
     * Pre-populate the cache of canEdit, canView, canDelete, canPublish permissions. This method will use the static
1266
     * can_(perm)_multiple method for efficiency.
1267
     *
1268
     * @param string          $permission    The permission: edit, view, publish, approve, etc.
1269
     * @param array           $ids           An array of page IDs
1270
     * @param callable|string $batchCallback The function/static method to call to calculate permissions.  Defaults
1271
     *                                       to 'SiteTree::can_(permission)_multiple'
1272
     */
1273
    public static function prepopulate_permission_cache($permission = 'CanEditType', $ids = [], $batchCallback = null)
1274
    {
1275
        if (!$batchCallback) {
1276
            $batchCallback = self::class . "::can_{$permission}_multiple";
1277
        }
1278
1279
        if (is_callable($batchCallback)) {
1280
            call_user_func($batchCallback, $ids, Member::currentUserID(), false);
1281
        } else {
1282
            user_error("SiteTree::prepopulate_permission_cache can't calculate '$permission' "
1283
                . "with callback '$batchCallback'", E_USER_WARNING);
1284
        }
1285
    }
1286
1287
    /**
1288
     * This method is NOT a full replacement for the individual can*() methods, e.g. {@link canEdit()}. Rather than
1289
     * checking (potentially slow) PHP logic, it relies on the database group associations, e.g. the "CanEditType" field
1290
     * plus the "SiteTree_EditorGroups" many-many table. By batch checking multiple records, we can combine the queries
1291
     * efficiently.
1292
     *
1293
     * Caches based on $typeField data. To invalidate the cache, use {@link SiteTree::reset()} or set the $useCached
1294
     * property to FALSE.
1295
     *
1296
     * @param array  $ids              Of {@link SiteTree} IDs
1297
     * @param int    $memberID         Member ID
1298
     * @param string $typeField        A property on the data record, e.g. "CanEditType".
1299
     * @param string $groupJoinTable   A many-many table name on this record, e.g. "SiteTree_EditorGroups"
1300
     * @param string $siteConfigMethod Method to call on {@link SiteConfig} for toplevel items, e.g. "canEdit"
1301
     * @param string $globalPermission If the member doesn't have this permission code, don't bother iterating deeper
1302
     * @param bool   $useCached
1303
     * @return array An map of {@link SiteTree} ID keys to boolean values
1304
     */
1305
    public static function batch_permission_check(
1306
        $ids,
1307
        $memberID,
1308
        $typeField,
1309
        $groupJoinTable,
1310
        $siteConfigMethod,
1311
        $globalPermission = null,
1312
        $useCached = true
1313
    ) {
1314
        if ($globalPermission === null) {
1315
            $globalPermission = array('CMS_ACCESS_LeftAndMain', 'CMS_ACCESS_CMSMain');
1316
        }
1317
1318
        // Sanitise the IDs
1319
        $ids = array_filter($ids, 'is_numeric');
1320
1321
        // This is the name used on the permission cache
1322
        // converts something like 'CanEditType' to 'edit'.
1323
        $cacheKey = strtolower(substr($typeField, 3, -4)) . "-$memberID";
1324
1325
        // Default result: nothing editable
1326
        $result = array_fill_keys($ids, false);
1327
        if ($ids) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $ids of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
1328
            // Look in the cache for values
1329
            if ($useCached && isset(self::$cache_permissions[$cacheKey])) {
1330
                $cachedValues = array_intersect_key(self::$cache_permissions[$cacheKey], $result);
1331
1332
                // If we can't find everything in the cache, then look up the remainder separately
1333
                $uncachedValues = array_diff_key($result, self::$cache_permissions[$cacheKey]);
1334
                if ($uncachedValues) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $uncachedValues of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
1335
                    $cachedValues = self::batch_permission_check(array_keys($uncachedValues), $memberID, $typeField, $groupJoinTable, $siteConfigMethod, $globalPermission, false) + $cachedValues;
0 ignored issues
show
Bug introduced by
It seems like $globalPermission defined by array('CMS_ACCESS_LeftAn..., 'CMS_ACCESS_CMSMain') on line 1315 can also be of type array<integer,string,{"0":"string","1":"string"}>; however, SilverStripe\CMS\Model\S...atch_permission_check() does only seem to accept string|null, maybe add an additional type check?

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

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

    return array();
}

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

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

Loading history...
1336
                }
1337
                return $cachedValues;
1338
            }
1339
1340
            // If a member doesn't have a certain permission then they can't edit anything
1341
            if (!$memberID || ($globalPermission && !Permission::checkMember($memberID, $globalPermission))) {
1342
                return $result;
1343
            }
1344
1345
            // Placeholder for parameterised ID list
1346
            $idPlaceholders = DB::placeholders($ids);
1347
1348
            // If page can't be viewed, don't grant edit permissions to do - implement can_view_multiple(), so this can
1349
            // be enabled
1350
            //$ids = array_keys(array_filter(self::can_view_multiple($ids, $memberID)));
1351
1352
            // Get the groups that the given member belongs to
1353
            /** @var Member $member */
1354
            $member = DataObject::get_by_id('SilverStripe\\Security\\Member', $memberID);
1355
            $groupIDs = $member->Groups()->column("ID");
1356
            $SQL_groupList = implode(", ", $groupIDs);
1357
            if (!$SQL_groupList) {
1358
                $SQL_groupList = '0';
1359
            }
1360
1361
            $combinedStageResult = array();
1362
1363
            foreach (array(Versioned::DRAFT, Versioned::LIVE) as $stage) {
1364
                // Start by filling the array with the pages that actually exist
1365
                /** @skipUpgrade */
1366
                $table = ($stage=='Stage') ? "SiteTree" : "SiteTree_$stage";
1367
1368
                if ($ids) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $ids of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
1369
                    $idQuery = "SELECT \"ID\" FROM \"$table\" WHERE \"ID\" IN ($idPlaceholders)";
1370
                    $stageIds = DB::prepared_query($idQuery, $ids)->column();
1371
                } else {
1372
                    $stageIds = array();
1373
                }
1374
                $result = array_fill_keys($stageIds, false);
1375
1376
                // Get the uninherited permissions
1377
                $uninheritedPermissions = Versioned::get_by_stage("SilverStripe\\CMS\\Model\\SiteTree", $stage)
1378
                    ->where(array(
1379
                        "(\"$typeField\" = 'LoggedInUsers' OR
1380
						(\"$typeField\" = 'OnlyTheseUsers' AND \"$groupJoinTable\".\"SiteTreeID\" IS NOT NULL))
1381
						AND \"SiteTree\".\"ID\" IN ($idPlaceholders)"
1382
                        => $ids
1383
                    ))
1384
                    ->leftJoin($groupJoinTable, "\"$groupJoinTable\".\"SiteTreeID\" = \"SiteTree\".\"ID\" AND \"$groupJoinTable\".\"GroupID\" IN ($SQL_groupList)");
1385
1386
                if ($uninheritedPermissions) {
1387
                    // Set all the relevant items in $result to true
1388
                    $result = array_fill_keys($uninheritedPermissions->column('ID'), true) + $result;
1389
                }
1390
1391
                // Get permissions that are inherited
1392
                $potentiallyInherited = Versioned::get_by_stage(
1393
                    "SilverStripe\\CMS\\Model\\SiteTree",
1394
                    $stage,
1395
                    array("\"$typeField\" = 'Inherit' AND \"SiteTree\".\"ID\" IN ($idPlaceholders)" => $ids)
0 ignored issues
show
Documentation introduced by
array("\"{$typeField}\" ...laceholders})" => $ids) is of type array<string|integer,array>, but the function expects a string.

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

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

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

<?php

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

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

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

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

}

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

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

See also the PhpDoc documentation for @property.

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

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

<?php

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

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

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

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

}

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

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

See also the PhpDoc documentation for @property.

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

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

<?php

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

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

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

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

}

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

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

See also the PhpDoc documentation for @property.

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

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

<?php

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

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

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

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

}

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

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

See also the PhpDoc documentation for @property.

Loading history...
1409
                        } else {
1410
                            // Might return different site config based on record context, e.g. when subsites module
1411
                            // is used
1412
                            $siteConfig = $item->getSiteConfig();
1413
                            $result[$item->ID] = $siteConfig->{$siteConfigMethod}($memberID);
1414
                        }
1415
                    }
1416
1417
                    if ($groupedByParent) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $groupedByParent of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
1418
                        $actuallyInherited = self::batch_permission_check(array_keys($groupedByParent), $memberID, $typeField, $groupJoinTable, $siteConfigMethod);
1419
                        if ($actuallyInherited) {
1420
                            $parentIDs = array_keys(array_filter($actuallyInherited));
1421
                            foreach ($parentIDs as $parentID) {
1422
                                // Set all the relevant items in $result to true
1423
                                $result = array_fill_keys($groupedByParent[$parentID], true) + $result;
1424
                            }
1425
                        }
1426
                    }
1427
                }
1428
1429
                $combinedStageResult = $combinedStageResult + $result;
1430
            }
1431
        }
1432
1433
        if (isset($combinedStageResult)) {
1434
            // Cache the results
1435
            if (empty(self::$cache_permissions[$cacheKey])) {
1436
                self::$cache_permissions[$cacheKey] = array();
1437
            }
1438
            self::$cache_permissions[$cacheKey] = $combinedStageResult + self::$cache_permissions[$cacheKey];
1439
            return $combinedStageResult;
1440
        } else {
1441
            return array();
1442
        }
1443
    }
1444
1445
    /**
1446
     * Get the 'can edit' information for a number of SiteTree pages.
1447
     *
1448
     * @param array $ids       An array of IDs of the SiteTree pages to look up
1449
     * @param int   $memberID  ID of member
1450
     * @param bool  $useCached Return values from the permission cache if they exist
1451
     * @return array A map where the IDs are keys and the values are booleans stating whether the given page can be
1452
     *                         edited
1453
     */
1454
    public static function can_edit_multiple($ids, $memberID, $useCached = true)
1455
    {
1456
        return self::batch_permission_check($ids, $memberID, 'CanEditType', 'SiteTree_EditorGroups', 'canEditPages', null, $useCached);
1457
    }
1458
1459
    /**
1460
     * Get the 'can edit' information for a number of SiteTree pages.
1461
     *
1462
     * @param array $ids       An array of IDs of the SiteTree pages to look up
1463
     * @param int   $memberID  ID of member
1464
     * @param bool  $useCached Return values from the permission cache if they exist
1465
     * @return array
1466
     */
1467
    public static function can_delete_multiple($ids, $memberID, $useCached = true)
1468
    {
1469
        $deletable = array();
1470
        $result = array_fill_keys($ids, false);
1471
        $cacheKey = "delete-$memberID";
1472
1473
        // Look in the cache for values
1474
        if ($useCached && isset(self::$cache_permissions[$cacheKey])) {
1475
            $cachedValues = array_intersect_key(self::$cache_permissions[$cacheKey], $result);
1476
1477
            // If we can't find everything in the cache, then look up the remainder separately
1478
            $uncachedValues = array_diff_key($result, self::$cache_permissions[$cacheKey]);
1479
            if ($uncachedValues) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $uncachedValues of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
1480
                $cachedValues = self::can_delete_multiple(array_keys($uncachedValues), $memberID, false)
1481
                    + $cachedValues;
1482
            }
1483
            return $cachedValues;
1484
        }
1485
1486
        // You can only delete pages that you can edit
1487
        $editableIDs = array_keys(array_filter(self::can_edit_multiple($ids, $memberID)));
1488
        if ($editableIDs) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $editableIDs of type array<integer|string> is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
1489
            // You can only delete pages whose children you can delete
1490
            $editablePlaceholders = DB::placeholders($editableIDs);
1491
            $childRecords = SiteTree::get()->where(array(
1492
                "\"SiteTree\".\"ParentID\" IN ($editablePlaceholders)" => $editableIDs
1493
            ));
1494
            if ($childRecords) {
1495
                $children = $childRecords->map("ID", "ParentID");
1496
1497
                // Find out the children that can be deleted
1498
                $deletableChildren = self::can_delete_multiple($children->keys(), $memberID);
1499
1500
                // Get a list of all the parents that have no undeletable children
1501
                $deletableParents = array_fill_keys($editableIDs, true);
1502
                foreach ($deletableChildren as $id => $canDelete) {
1503
                    if (!$canDelete) {
1504
                        unset($deletableParents[$children[$id]]);
1505
                    }
1506
                }
1507
1508
                // Use that to filter the list of deletable parents that have children
1509
                $deletableParents = array_keys($deletableParents);
1510
1511
                // Also get the $ids that don't have children
1512
                $parents = array_unique($children->values());
1513
                $deletableLeafNodes = array_diff($editableIDs, $parents);
1514
1515
                // Combine the two
1516
                $deletable = array_merge($deletableParents, $deletableLeafNodes);
1517
            } else {
1518
                $deletable = $editableIDs;
1519
            }
1520
        }
1521
1522
        // Convert the array of deletable IDs into a map of the original IDs with true/false as the value
1523
        return array_fill_keys($deletable, true) + array_fill_keys($ids, false);
1524
    }
1525
1526
    /**
1527
     * Collate selected descendants of this page.
1528
     *
1529
     * {@link $condition} will be evaluated on each descendant, and if it is succeeds, that item will be added to the
1530
     * $collator array.
1531
     *
1532
     * @param string $condition The PHP condition to be evaluated. The page will be called $item
1533
     * @param array  $collator  An array, passed by reference, to collect all of the matching descendants.
1534
     * @return bool
1535
     */
1536
    public function collateDescendants($condition, &$collator)
1537
    {
1538
        $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...
1539
        if ($children) {
1540
            foreach ($children as $item) {
1541
                if (eval("return $condition;")) {
1542
                    $collator[] = $item;
1543
                }
1544
                /** @var SiteTree $item */
1545
                $item->collateDescendants($condition, $collator);
1546
            }
1547
            return true;
1548
        }
1549
        return false;
1550
    }
1551
1552
    /**
1553
     * Return the title, description, keywords and language metatags.
1554
     *
1555
     * @todo Move <title> tag in separate getter for easier customization and more obvious usage
1556
     *
1557
     * @param bool $includeTitle Show default <title>-tag, set to false for custom templating
1558
     * @return string The XHTML metatags
1559
     */
1560
    public function MetaTags($includeTitle = true)
1561
    {
1562
        $tags = array();
1563
        if ($includeTitle && strtolower($includeTitle) != 'false') {
1564
            $tags[] = FormField::create_tag('title', array(), $this->obj('Title')->forTemplate());
1565
        }
1566
1567
        $generator = trim(Config::inst()->get(self::class, 'meta_generator'));
1568
        if (!empty($generator)) {
1569
            $tags[] = FormField::create_tag('meta', array(
1570
                'name' => 'generator',
1571
                'content' => $generator,
1572
            ));
1573
        }
1574
1575
        $charset = ContentNegotiator::config()->get('encoding');
1576
        $tags[] = FormField::create_tag('meta', array(
1577
            'http-equiv' => 'Content-Type',
1578
            'content' => 'text/html; charset=' . $charset,
1579
        ));
1580
        if ($this->MetaDescription) {
1581
            $tags[] = FormField::create_tag('meta', array(
1582
                'name' => 'description',
1583
                'content' => $this->MetaDescription,
1584
            ));
1585
        }
1586
1587
        if (Permission::check('CMS_ACCESS_CMSMain')
1588
            && !$this instanceof ErrorPage
1589
            && $this->ID > 0
1590
        ) {
1591
            $tags[] = FormField::create_tag('meta', array(
1592
                'name' => 'x-page-id',
1593
                'content' => $this->obj('ID')->forTemplate(),
1594
            ));
1595
            $tags[] = FormField::create_tag('meta', array(
1596
                'name' => 'x-cms-edit-link',
1597
                'content' => $this->obj('CMSEditLink')->forTemplate(),
1598
            ));
1599
        }
1600
1601
        $tags = implode("\n", $tags);
1602
        if ($this->ExtraMeta) {
1603
            $tags .= $this->obj('ExtraMeta')->forTemplate();
1604
        }
1605
1606
        $this->extend('MetaTags', $tags);
1607
1608
        return $tags;
1609
    }
1610
1611
    /**
1612
     * Returns the object that contains the content that a user would associate with this page.
1613
     *
1614
     * Ordinarily, this is just the page itself, but for example on RedirectorPages or VirtualPages ContentSource() will
1615
     * return the page that is linked to.
1616
     *
1617
     * @return $this
1618
     */
1619
    public function ContentSource()
1620
    {
1621
        return $this;
1622
    }
1623
1624
    /**
1625
     * Add default records to database.
1626
     *
1627
     * This function is called whenever the database is built, after the database tables have all been created. Overload
1628
     * this to add default records when the database is built, but make sure you call parent::requireDefaultRecords().
1629
     */
1630
    public function requireDefaultRecords()
1631
    {
1632
        parent::requireDefaultRecords();
1633
1634
        // default pages
1635
        if (static::class == self::class && $this->config()->create_default_pages) {
1636
            if (!SiteTree::get_by_link(RootURLController::config()->default_homepage_link)) {
1637
                $homepage = new Page();
1638
                $homepage->Title = _t('SiteTree.DEFAULTHOMETITLE', 'Home');
1639
                $homepage->Content = _t('SiteTree.DEFAULTHOMECONTENT', '<p>Welcome to SilverStripe! This is the default homepage. You can edit this page by opening <a href="admin/">the CMS</a>.</p><p>You can now access the <a href="http://docs.silverstripe.org">developer documentation</a>, or begin the <a href="http://www.silverstripe.org/learn/lessons">SilverStripe lessons</a>.</p>');
1640
                $homepage->URLSegment = RootURLController::config()->default_homepage_link;
1641
                $homepage->Sort = 1;
1642
                $homepage->write();
1643
                $homepage->copyVersionToStage(Versioned::DRAFT, Versioned::LIVE);
1644
                $homepage->flushCache();
1645
                DB::alteration_message('Home page created', 'created');
1646
            }
1647
1648
            if (DB::query("SELECT COUNT(*) FROM \"SiteTree\"")->value() == 1) {
1649
                $aboutus = new Page();
1650
                $aboutus->Title = _t('SiteTree.DEFAULTABOUTTITLE', 'About Us');
1651
                $aboutus->Content = _t(
1652
                    'SiteTree.DEFAULTABOUTCONTENT',
1653
                    '<p>You can fill this page out with your own content, or delete it and create your own pages.</p>'
1654
                );
1655
                $aboutus->Sort = 2;
1656
                $aboutus->write();
1657
                $aboutus->copyVersionToStage(Versioned::DRAFT, Versioned::LIVE);
1658
                $aboutus->flushCache();
1659
                DB::alteration_message('About Us page created', 'created');
1660
1661
                $contactus = new Page();
1662
                $contactus->Title = _t('SiteTree.DEFAULTCONTACTTITLE', 'Contact Us');
1663
                $contactus->Content = _t(
1664
                    'SiteTree.DEFAULTCONTACTCONTENT',
1665
                    '<p>You can fill this page out with your own content, or delete it and create your own pages.</p>'
1666
                );
1667
                $contactus->Sort = 3;
1668
                $contactus->write();
1669
                $contactus->copyVersionToStage(Versioned::DRAFT, Versioned::LIVE);
1670
                $contactus->flushCache();
1671
                DB::alteration_message('Contact Us page created', 'created');
1672
            }
1673
        }
1674
    }
1675
1676
    protected function onBeforeWrite()
1677
    {
1678
        parent::onBeforeWrite();
1679
1680
        // If Sort hasn't been set, make this page come after it's siblings
1681
        if (!$this->Sort) {
1682
            $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...
1683
            $this->Sort = DB::prepared_query(
1684
                "SELECT MAX(\"Sort\") + 1 FROM \"SiteTree\" WHERE \"ParentID\" = ?",
1685
                array($parentID)
1686
            )->value();
1687
        }
1688
1689
        // If there is no URLSegment set, generate one from Title
1690
        $defaultSegment = $this->generateURLSegment(_t(
1691
            'CMSMain.NEWPAGE',
1692
            'New {pagetype}',
1693
            array('pagetype' => $this->i18n_singular_name())
0 ignored issues
show
Documentation introduced by
array('pagetype' => $this->i18n_singular_name()) is of type array<string,string,{"pagetype":"string"}>, but the function expects a string.

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1694
        ));
1695
        if ((!$this->URLSegment || $this->URLSegment == $defaultSegment) && $this->Title) {
1696
            $this->URLSegment = $this->generateURLSegment($this->Title);
1697
        } elseif ($this->isChanged('URLSegment', 2)) {
1698
            // Do a strict check on change level, to avoid double encoding caused by
1699
            // bogus changes through forceChange()
1700
            $filter = URLSegmentFilter::create();
1701
            $this->URLSegment = $filter->filter($this->URLSegment);
1702
            // If after sanitising there is no URLSegment, give it a reasonable default
1703
            if (!$this->URLSegment) {
1704
                $this->URLSegment = "page-$this->ID";
1705
            }
1706
        }
1707
1708
        // Ensure that this object has a non-conflicting URLSegment value.
1709
        $count = 2;
1710
        while (!$this->validURLSegment()) {
1711
            $this->URLSegment = preg_replace('/-[0-9]+$/', null, $this->URLSegment) . '-' . $count;
1712
            $count++;
1713
        }
1714
1715
        $this->syncLinkTracking();
1716
1717
        // Check to see if we've only altered fields that shouldn't affect versioning
1718
        $fieldsIgnoredByVersioning = array('HasBrokenLink', 'Status', 'HasBrokenFile', 'ToDo', 'VersionID', 'SaveCount');
1719
        $changedFields = array_keys($this->getChangedFields(true, 2));
1720
1721
        // This more rigorous check is inline with the test that write() does to decide whether or not to write to the
1722
        // DB. We use that to avoid cluttering the system with a migrateVersion() call that doesn't get used
1723
        $oneChangedFields = array_keys($this->getChangedFields(true, 1));
1724
1725
        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...
1726
            // This will have the affect of preserving the versioning
1727
            $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...
1728
        }
1729
    }
1730
1731
    /**
1732
     * Trigger synchronisation of link tracking
1733
     *
1734
     * {@see SiteTreeLinkTracking::augmentSyncLinkTracking}
1735
     */
1736
    public function syncLinkTracking()
1737
    {
1738
        $this->extend('augmentSyncLinkTracking');
1739
    }
1740
1741
    public function onBeforeDelete()
1742
    {
1743
        parent::onBeforeDelete();
1744
1745
        // If deleting this page, delete all its children.
1746
        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...
1747
            foreach ($children as $child) {
1748
                /** @var SiteTree $child */
1749
                $child->delete();
1750
            }
1751
        }
1752
    }
1753
1754
    public function onAfterDelete()
1755
    {
1756
        // Need to flush cache to avoid outdated versionnumber references
1757
        $this->flushCache();
1758
1759
        // Need to mark pages depending to this one as broken
1760
        $dependentPages = $this->DependentPages();
1761
        if ($dependentPages) {
1762
            foreach ($dependentPages as $page) {
1763
            // $page->write() calls syncLinkTracking, which does all the hard work for us.
1764
                $page->write();
1765
            }
1766
        }
1767
1768
        parent::onAfterDelete();
1769
    }
1770
1771
    public function flushCache($persistent = true)
1772
    {
1773
        parent::flushCache($persistent);
1774
        $this->_cache_statusFlags = null;
1775
    }
1776
1777
    public function validate()
1778
    {
1779
        $result = parent::validate();
1780
1781
        // Allowed children validation
1782
        $parent = $this->getParent();
1783
        if ($parent && $parent->exists()) {
1784
            // No need to check for subclasses or instanceof, as allowedChildren() already
1785
            // deconstructs any inheritance trees already.
1786
            $allowed = $parent->allowedChildren();
1787
            $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...
1788
                ? $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...
1789
                : $this;
1790
            if (!in_array($subject->ClassName, $allowed)) {
1791
                $result->addError(
1792
                    _t(
1793
                        'SiteTree.PageTypeNotAllowed',
1794
                        'Page type "{type}" not allowed as child of this parent page',
1795
                        array('type' => $subject->i18n_singular_name())
0 ignored issues
show
Documentation introduced by
array('type' => $subject->i18n_singular_name()) is of type array<string,?,{"type":"?"}>, but the function expects a string.

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1796
                    ),
1797
                    ValidationResult::TYPE_ERROR,
1798
                    'ALLOWED_CHILDREN'
1799
                );
1800
            }
1801
        }
1802
1803
        // "Can be root" validation
1804
        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...
1805
            $result->addError(
1806
                _t(
1807
                    'SiteTree.PageTypNotAllowedOnRoot',
1808
                    'Page type "{type}" is not allowed on the root level',
1809
                    array('type' => $this->i18n_singular_name())
0 ignored issues
show
Documentation introduced by
array('type' => $this->i18n_singular_name()) is of type array<string,string,{"type":"string"}>, but the function expects a string.

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1810
                ),
1811
                ValidationResult::TYPE_ERROR,
1812
                'CAN_BE_ROOT'
1813
            );
1814
        }
1815
1816
        return $result;
1817
    }
1818
1819
    /**
1820
     * Returns true if this object has a URLSegment value that does not conflict with any other objects. This method
1821
     * checks for:
1822
     *  - A page with the same URLSegment that has a conflict
1823
     *  - Conflicts with actions on the parent page
1824
     *  - A conflict caused by a root page having the same URLSegment as a class name
1825
     *
1826
     * @return bool
1827
     */
1828
    public function validURLSegment()
1829
    {
1830
        if (self::config()->nested_urls && $parent = $this->Parent()) {
1831
            if ($controller = ModelAsController::controller_for($parent)) {
1832
                if ($controller instanceof Controller && $controller->hasAction($this->URLSegment)) {
1833
                    return false;
1834
                }
1835
            }
1836
        }
1837
1838
        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...
1839
            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...
1840
                return false;
1841
            }
1842
        }
1843
1844
        // Filters by url, id, and parent
1845
        $filter = array('"SiteTree"."URLSegment"' => $this->URLSegment);
1846
        if ($this->ID) {
1847
            $filter['"SiteTree"."ID" <> ?'] = $this->ID;
1848
        }
1849
        if (self::config()->nested_urls) {
1850
            $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...
1851
        }
1852
1853
        // If any of the extensions return `0` consider the segment invalid
1854
        $extensionResponses = array_filter(
1855
            (array)$this->extend('augmentValidURLSegment'),
1856
            function ($response) {
1857
                return !is_null($response);
1858
            }
1859
        );
1860
        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...
1861
            return min($extensionResponses);
1862
        }
1863
1864
        // Check existence
1865
        return !DataObject::get(self::class, $filter)->exists();
1866
    }
1867
1868
    /**
1869
     * Generate a URL segment based on the title provided.
1870
     *
1871
     * If {@link Extension}s wish to alter URL segment generation, they can do so by defining
1872
     * updateURLSegment(&$url, $title).  $url will be passed by reference and should be modified. $title will contain
1873
     * the title that was originally used as the source of this generated URL. This lets extensions either start from
1874
     * scratch, or incrementally modify the generated URL.
1875
     *
1876
     * @param string $title Page title
1877
     * @return string Generated url segment
1878
     */
1879
    public function generateURLSegment($title)
1880
    {
1881
        $filter = URLSegmentFilter::create();
1882
        $t = $filter->filter($title);
1883
1884
        // Fallback to generic page name if path is empty (= no valid, convertable characters)
1885
        if (!$t || $t == '-' || $t == '-1') {
1886
            $t = "page-$this->ID";
1887
        }
1888
1889
        // Hook for extensions
1890
        $this->extend('updateURLSegment', $t, $title);
1891
1892
        return $t;
1893
    }
1894
1895
    /**
1896
     * Gets the URL segment for the latest draft version of this page.
1897
     *
1898
     * @return string
1899
     */
1900
    public function getStageURLSegment()
1901
    {
1902
        $stageRecord = Versioned::get_one_by_stage(self::class, Versioned::DRAFT, array(
0 ignored issues
show
Documentation introduced by
array('"SiteTree"."ID"' => $this->ID) is of type array<string,integer|str...D\"":"integer|string"}>, but the function expects a string.

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1903
            '"SiteTree"."ID"' => $this->ID
1904
        ));
1905
        return ($stageRecord) ? $stageRecord->URLSegment : null;
1906
    }
1907
1908
    /**
1909
     * Gets the URL segment for the currently published version of this page.
1910
     *
1911
     * @return string
1912
     */
1913
    public function getLiveURLSegment()
1914
    {
1915
        $liveRecord = Versioned::get_one_by_stage(self::class, Versioned::LIVE, array(
0 ignored issues
show
Documentation introduced by
array('"SiteTree"."ID"' => $this->ID) is of type array<string,integer|str...D\"":"integer|string"}>, but the function expects a string.

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1916
            '"SiteTree"."ID"' => $this->ID
1917
        ));
1918
        return ($liveRecord) ? $liveRecord->URLSegment : null;
1919
    }
1920
1921
    /**
1922
     * Returns the pages that depend on this page. This includes virtual pages, pages that link to it, etc.
1923
     *
1924
     * @param bool $includeVirtuals Set to false to exlcude virtual pages.
1925
     * @return ArrayList
1926
     */
1927
    public function DependentPages($includeVirtuals = true)
1928
    {
1929
        if (class_exists('Subsite')) {
1930
            $origDisableSubsiteFilter = Subsite::$disable_subsite_filter;
1931
            Subsite::disable_subsite_filter(true);
1932
        }
1933
1934
        // Content links
1935
        $items = new ArrayList();
1936
1937
        // We merge all into a regular SS_List, because DataList doesn't support merge
1938
        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...
1939
            $linkList = new ArrayList();
1940
            foreach ($contentLinks as $item) {
1941
                $item->DependentLinkType = 'Content link';
1942
                $linkList->push($item);
1943
            }
1944
            $items->merge($linkList);
1945
        }
1946
1947
        // Virtual pages
1948
        if ($includeVirtuals) {
1949
            $virtuals = $this->VirtualPages();
1950
            if ($virtuals) {
1951
                $virtualList = new ArrayList();
1952
                foreach ($virtuals as $item) {
1953
                    $item->DependentLinkType = 'Virtual page';
1954
                    $virtualList->push($item);
1955
                }
1956
                $items->merge($virtualList);
1957
            }
1958
        }
1959
1960
        // Redirector pages
1961
        $redirectors = RedirectorPage::get()->where(array(
1962
            '"RedirectorPage"."RedirectionType"' => 'Internal',
1963
            '"RedirectorPage"."LinkToID"' => $this->ID
1964
        ));
1965
        if ($redirectors) {
1966
            $redirectorList = new ArrayList();
1967
            foreach ($redirectors as $item) {
1968
                $item->DependentLinkType = 'Redirector page';
1969
                $redirectorList->push($item);
1970
            }
1971
            $items->merge($redirectorList);
1972
        }
1973
1974
        if (class_exists('Subsite')) {
1975
            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...
1976
        }
1977
1978
        return $items;
1979
    }
1980
1981
    /**
1982
     * Return all virtual pages that link to this page.
1983
     *
1984
     * @return DataList
1985
     */
1986
    public function VirtualPages()
1987
    {
1988
        $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...
1989
1990
        // Disable subsite filter for these pages
1991
        if ($pages instanceof DataList) {
1992
            return $pages->setDataQueryParam('Subsite.filter', false);
1993
        } else {
1994
            return $pages;
1995
        }
1996
    }
1997
1998
    /**
1999
     * Returns a FieldList with which to create the main editing form.
2000
     *
2001
     * You can override this in your child classes to add extra fields - first get the parent fields using
2002
     * parent::getCMSFields(), then use addFieldToTab() on the FieldList.
2003
     *
2004
     * See {@link getSettingsFields()} for a different set of fields concerned with configuration aspects on the record,
2005
     * e.g. access control.
2006
     *
2007
     * @return FieldList The fields to be displayed in the CMS
2008
     */
2009
    public function getCMSFields()
2010
    {
2011
        // Status / message
2012
        // Create a status message for multiple parents
2013
        if ($this->ID && is_numeric($this->ID)) {
2014
            $linkedPages = $this->VirtualPages();
2015
2016
            $parentPageLinks = array();
2017
2018
            if ($linkedPages->count() > 0) {
2019
                /** @var VirtualPage $linkedPage */
2020
                foreach ($linkedPages as $linkedPage) {
2021
                    $parentPage = $linkedPage->Parent();
2022
                    if ($parentPage && $parentPage->exists()) {
2023
                        $link = Convert::raw2att($parentPage->CMSEditLink());
2024
                        $title = Convert::raw2xml($parentPage->Title);
2025
                    } else {
2026
                        $link = CMSPageEditController::singleton()->Link('show');
2027
                        $title = _t('SiteTree.TOPLEVEL', 'Site Content (Top Level)');
2028
                    }
2029
                    $parentPageLinks[] = "<a class=\"cmsEditlink\" href=\"{$link}\">{$title}</a>";
2030
                }
2031
2032
                $lastParent = array_pop($parentPageLinks);
2033
                $parentList = "'$lastParent'";
2034
2035
                if (count($parentPageLinks)) {
2036
                    $parentList = "'" . implode("', '", $parentPageLinks) . "' and "
2037
                        . $parentList;
2038
                }
2039
2040
                $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...
2041
                    'SiteTree.APPEARSVIRTUALPAGES',
2042
                    "This content also appears on the virtual pages in the {title} sections.",
2043
                    array('title' => $parentList)
0 ignored issues
show
Documentation introduced by
array('title' => $parentList) is of type array<string,?,{"title":"?"}>, but the function expects a string.

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
2044
                );
2045
            }
2046
        }
2047
2048
        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...
2049
            $statusMessage[] = _t('SiteTree.HASBROKENLINKS', "This page has broken links.");
0 ignored issues
show
Bug introduced by
The variable $statusMessage does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
2050
        }
2051
2052
        $dependentNote = '';
2053
        $dependentTable = new LiteralField('DependentNote', '<p></p>');
2054
2055
        // Create a table for showing pages linked to this one
2056
        $dependentPages = $this->DependentPages();
2057
        $dependentPagesCount = $dependentPages->count();
2058
        if ($dependentPagesCount) {
2059
            $dependentColumns = array(
2060
                'Title' => $this->fieldLabel('Title'),
2061
                'AbsoluteLink' => _t('SiteTree.DependtPageColumnURL', 'URL'),
2062
                'DependentLinkType' => _t('SiteTree.DependtPageColumnLinkType', 'Link type'),
2063
            );
2064
            if (class_exists('Subsite')) {
2065
                $dependentColumns['Subsite.Title'] = singleton('Subsite')->i18n_singular_name();
2066
            }
2067
2068
            $dependentNote = new LiteralField('DependentNote', '<p>' . _t('SiteTree.DEPENDENT_NOTE', 'The following pages depend on this page. This includes virtual pages, redirector pages, and pages with content links.') . '</p>');
2069
            $dependentTable = GridField::create(
2070
                'DependentPages',
2071
                false,
2072
                $dependentPages
2073
            );
2074
            /** @var GridFieldDataColumns $dataColumns */
2075
            $dataColumns = $dependentTable->getConfig()->getComponentByType('SilverStripe\\Forms\\GridField\\GridFieldDataColumns');
2076
            $dataColumns
2077
                ->setDisplayFields($dependentColumns)
2078
                ->setFieldFormatting(array(
2079
                    'Title' => function ($value, &$item) {
2080
                        return sprintf(
2081
                            '<a href="admin/pages/edit/show/%d">%s</a>',
2082
                            (int)$item->ID,
2083
                            Convert::raw2xml($item->Title)
2084
                        );
2085
                    },
2086
                    '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...
2087
                        return sprintf(
2088
                            '<a href="%s" target="_blank">%s</a>',
2089
                            Convert::raw2xml($value),
2090
                            Convert::raw2xml($value)
2091
                        );
2092
                    }
2093
                ));
2094
        }
2095
2096
        $baseLink = Controller::join_links(
2097
            Director::absoluteBaseURL(),
2098
            (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...
2099
        );
2100
2101
        $urlsegment = SiteTreeURLSegmentField::create("URLSegment", $this->fieldLabel('URLSegment'))
2102
            ->setURLPrefix($baseLink)
2103
            ->setDefaultURL($this->generateURLSegment(_t(
2104
                'CMSMain.NEWPAGE',
2105
                'New {pagetype}',
2106
                array('pagetype' => $this->i18n_singular_name())
0 ignored issues
show
Documentation introduced by
array('pagetype' => $this->i18n_singular_name()) is of type array<string,string,{"pagetype":"string"}>, but the function expects a string.

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
2107
            )));
2108
        $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...
2109
            ? $this->fieldLabel('LinkChangeNote')
2110
            : '';
2111
        if (!Config::inst()->get('SilverStripe\\View\\Parsers\\URLSegmentFilter', 'default_allow_multibyte')) {
2112
            $helpText .= _t('SiteTreeURLSegmentField.HelpChars', ' Special characters are automatically converted or removed.');
2113
        }
2114
        $urlsegment->setHelpText($helpText);
2115
2116
        $fields = new FieldList(
2117
            $rootTab = new TabSet(
2118
                "Root",
2119
                $tabMain = new Tab(
2120
                    'Main',
2121
                    new TextField("Title", $this->fieldLabel('Title')),
2122
                    $urlsegment,
2123
                    new TextField("MenuTitle", $this->fieldLabel('MenuTitle')),
2124
                    $htmlField = new HTMLEditorField("Content", _t('SiteTree.HTMLEDITORTITLE', "Content", 'HTML editor title')),
2125
                    ToggleCompositeField::create(
2126
                        'Metadata',
2127
                        _t('SiteTree.MetadataToggle', 'Metadata'),
2128
                        array(
2129
                            $metaFieldDesc = new TextareaField("MetaDescription", $this->fieldLabel('MetaDescription')),
2130
                            $metaFieldExtra = new TextareaField("ExtraMeta", $this->fieldLabel('ExtraMeta'))
2131
                        )
2132
                    )->setHeadingLevel(4)
2133
                ),
2134
                $tabDependent = new Tab(
2135
                    'Dependent',
2136
                    $dependentNote,
2137
                    $dependentTable
2138
                )
2139
            )
2140
        );
2141
        $htmlField->addExtraClass('stacked');
2142
2143
        // Help text for MetaData on page content editor
2144
        $metaFieldDesc
2145
            ->setRightTitle(
2146
                _t(
2147
                    'SiteTree.METADESCHELP',
2148
                    "Search engines use this content for displaying search results (although it will not influence their ranking)."
2149
                )
2150
            )
2151
            ->addExtraClass('help');
2152
        $metaFieldExtra
2153
            ->setRightTitle(
2154
                _t(
2155
                    'SiteTree.METAEXTRAHELP',
2156
                    "HTML tags for additional meta information. For example &lt;meta name=\"customName\" content=\"your custom content here\" /&gt;"
2157
                )
2158
            )
2159
            ->addExtraClass('help');
2160
2161
        // Conditional dependent pages tab
2162
        if ($dependentPagesCount) {
2163
            $tabDependent->setTitle(_t('SiteTree.TABDEPENDENT', "Dependent pages") . " ($dependentPagesCount)");
2164
        } else {
2165
            $fields->removeFieldFromTab('Root', 'Dependent');
2166
        }
2167
2168
        $tabMain->setTitle(_t('SiteTree.TABCONTENT', "Main Content"));
2169
2170
        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...
2171
            $obsoleteWarning = _t(
2172
                'SiteTree.OBSOLETECLASS',
2173
                "This page is of obsolete type {type}. Saving will reset its type and you may lose data",
2174
                array('type' => $this->ObsoleteClassName)
0 ignored issues
show
Bug introduced by
The property ObsoleteClassName does not seem to exist. Did you mean ClassName?

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

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

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

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
2175
            );
2176
2177
            $fields->addFieldToTab(
2178
                "Root.Main",
2179
                new LiteralField("ObsoleteWarningHeader", "<p class=\"message warning\">$obsoleteWarning</p>"),
2180
                "Title"
2181
            );
2182
        }
2183
2184
        if (file_exists(BASE_PATH . '/install.php')) {
2185
            $fields->addFieldToTab("Root.Main", new LiteralField(
2186
                "InstallWarningHeader",
2187
                "<p class=\"message warning\">" . _t(
2188
                    "SiteTree.REMOVE_INSTALL_WARNING",
2189
                    "Warning: You should remove install.php from this SilverStripe install for security reasons."
2190
                )
2191
                . "</p>"
2192
            ), "Title");
2193
        }
2194
2195
        if (self::$runCMSFieldsExtensions) {
2196
            $this->extend('updateCMSFields', $fields);
2197
        }
2198
2199
        return $fields;
2200
    }
2201
2202
2203
    /**
2204
     * Returns fields related to configuration aspects on this record, e.g. access control. See {@link getCMSFields()}
2205
     * for content-related fields.
2206
     *
2207
     * @return FieldList
2208
     */
2209
    public function getSettingsFields()
2210
    {
2211
        $groupsMap = array();
2212
        foreach (Group::get() as $group) {
2213
            // Listboxfield values are escaped, use ASCII char instead of &raquo;
2214
            $groupsMap[$group->ID] = $group->getBreadcrumbs(' > ');
2215
        }
2216
        asort($groupsMap);
2217
2218
        $fields = new FieldList(
2219
            $rootTab = new TabSet(
2220
                "Root",
2221
                $tabBehaviour = new Tab(
2222
                    'Settings',
2223
                    new DropdownField(
2224
                        "ClassName",
2225
                        $this->fieldLabel('ClassName'),
2226
                        $this->getClassDropdown()
2227
                    ),
2228
                    $parentTypeSelector = new CompositeField(
2229
                        $parentType = new OptionsetField("ParentType", _t("SiteTree.PAGELOCATION", "Page location"), array(
2230
                            "root" => _t("SiteTree.PARENTTYPE_ROOT", "Top-level page"),
2231
                            "subpage" => _t("SiteTree.PARENTTYPE_SUBPAGE", "Sub-page underneath a parent page"),
2232
                        )),
2233
                        $parentIDField = new TreeDropdownField("ParentID", $this->fieldLabel('ParentID'), self::class, 'ID', 'MenuTitle')
2234
                    ),
2235
                    $visibility = new FieldGroup(
2236
                        new CheckboxField("ShowInMenus", $this->fieldLabel('ShowInMenus')),
2237
                        new CheckboxField("ShowInSearch", $this->fieldLabel('ShowInSearch'))
2238
                    ),
2239
                    $viewersOptionsField = new OptionsetField(
2240
                        "CanViewType",
2241
                        _t('SiteTree.ACCESSHEADER', "Who can view this page?")
2242
                    ),
2243
                    $viewerGroupsField = ListboxField::create("ViewerGroups", _t('SiteTree.VIEWERGROUPS', "Viewer Groups"))
2244
                        ->setSource($groupsMap)
2245
                        ->setAttribute(
2246
                            'data-placeholder',
2247
                            _t('SiteTree.GroupPlaceholder', 'Click to select group')
2248
                        ),
2249
                    $editorsOptionsField = new OptionsetField(
2250
                        "CanEditType",
2251
                        _t('SiteTree.EDITHEADER', "Who can edit this page?")
2252
                    ),
2253
                    $editorGroupsField = ListboxField::create("EditorGroups", _t('SiteTree.EDITORGROUPS', "Editor Groups"))
2254
                        ->setSource($groupsMap)
2255
                        ->setAttribute(
2256
                            'data-placeholder',
2257
                            _t('SiteTree.GroupPlaceholder', 'Click to select group')
2258
                        )
2259
                )
2260
            )
2261
        );
2262
2263
        $parentType->addExtraClass('noborder');
2264
        $visibility->setTitle($this->fieldLabel('Visibility'));
2265
2266
2267
        // This filter ensures that the ParentID dropdown selection does not show this node,
2268
        // or its descendents, as this causes vanishing bugs
2269
        $parentIDField->setFilterFunction(create_function('$node', "return \$node->ID != {$this->ID};"));
2270
        $parentTypeSelector->addExtraClass('parentTypeSelector');
2271
2272
        $tabBehaviour->setTitle(_t('SiteTree.TABBEHAVIOUR', "Behavior"));
2273
2274
        // Make page location fields read-only if the user doesn't have the appropriate permission
2275
        if (!Permission::check("SITETREE_REORGANISE")) {
2276
            $fields->makeFieldReadonly('ParentType');
2277
            if ($this->getParentType() === 'root') {
2278
                $fields->removeByName('ParentID');
2279
            } else {
2280
                $fields->makeFieldReadonly('ParentID');
2281
            }
2282
        }
2283
2284
        $viewersOptionsSource = array();
2285
        $viewersOptionsSource["Inherit"] = _t('SiteTree.INHERIT', "Inherit from parent page");
2286
        $viewersOptionsSource["Anyone"] = _t('SiteTree.ACCESSANYONE', "Anyone");
2287
        $viewersOptionsSource["LoggedInUsers"] = _t('SiteTree.ACCESSLOGGEDIN', "Logged-in users");
2288
        $viewersOptionsSource["OnlyTheseUsers"] = _t('SiteTree.ACCESSONLYTHESE', "Only these people (choose from list)");
2289
        $viewersOptionsField->setSource($viewersOptionsSource);
2290
2291
        $editorsOptionsSource = array();
2292
        $editorsOptionsSource["Inherit"] = _t('SiteTree.INHERIT', "Inherit from parent page");
2293
        $editorsOptionsSource["LoggedInUsers"] = _t('SiteTree.EDITANYONE', "Anyone who can log-in to the CMS");
2294
        $editorsOptionsSource["OnlyTheseUsers"] = _t('SiteTree.EDITONLYTHESE', "Only these people (choose from list)");
2295
        $editorsOptionsField->setSource($editorsOptionsSource);
2296
2297
        if (!Permission::check('SITETREE_GRANT_ACCESS')) {
2298
            $fields->makeFieldReadonly($viewersOptionsField);
2299
            if ($this->CanViewType == 'OnlyTheseUsers') {
2300
                $fields->makeFieldReadonly($viewerGroupsField);
2301
            } else {
2302
                $fields->removeByName('ViewerGroups');
2303
            }
2304
2305
            $fields->makeFieldReadonly($editorsOptionsField);
2306
            if ($this->CanEditType == 'OnlyTheseUsers') {
2307
                $fields->makeFieldReadonly($editorGroupsField);
2308
            } else {
2309
                $fields->removeByName('EditorGroups');
2310
            }
2311
        }
2312
2313
        if (self::$runCMSFieldsExtensions) {
2314
            $this->extend('updateSettingsFields', $fields);
2315
        }
2316
2317
        return $fields;
2318
    }
2319
2320
    /**
2321
     * @param bool $includerelations A boolean value to indicate if the labels returned should include relation fields
2322
     * @return array
2323
     */
2324
    public function fieldLabels($includerelations = true)
2325
    {
2326
        $cacheKey = static::class . '_' . $includerelations;
2327
        if (!isset(self::$_cache_field_labels[$cacheKey])) {
2328
            $labels = parent::fieldLabels($includerelations);
2329
            $labels['Title'] = _t('SiteTree.PAGETITLE', "Page name");
2330
            $labels['MenuTitle'] = _t('SiteTree.MENUTITLE', "Navigation label");
2331
            $labels['MetaDescription'] = _t('SiteTree.METADESC', "Meta Description");
2332
            $labels['ExtraMeta'] = _t('SiteTree.METAEXTRA', "Custom Meta Tags");
2333
            $labels['ClassName'] = _t('SiteTree.PAGETYPE', "Page type", 'Classname of a page object');
2334
            $labels['ParentType'] = _t('SiteTree.PARENTTYPE', "Page location");
2335
            $labels['ParentID'] = _t('SiteTree.PARENTID', "Parent page");
2336
            $labels['ShowInMenus'] =_t('SiteTree.SHOWINMENUS', "Show in menus?");
2337
            $labels['ShowInSearch'] = _t('SiteTree.SHOWINSEARCH', "Show in search?");
2338
            $labels['ProvideComments'] = _t('SiteTree.ALLOWCOMMENTS', "Allow comments on this page?");
2339
            $labels['ViewerGroups'] = _t('SiteTree.VIEWERGROUPS', "Viewer Groups");
2340
            $labels['EditorGroups'] = _t('SiteTree.EDITORGROUPS', "Editor Groups");
2341
            $labels['URLSegment'] = _t('SiteTree.URLSegment', 'URL Segment', 'URL for this page');
2342
            $labels['Content'] = _t('SiteTree.Content', 'Content', 'Main HTML Content for a page');
2343
            $labels['CanViewType'] = _t('SiteTree.Viewers', 'Viewers Groups');
2344
            $labels['CanEditType'] = _t('SiteTree.Editors', 'Editors Groups');
2345
            $labels['Comments'] = _t('SiteTree.Comments', 'Comments');
2346
            $labels['Visibility'] = _t('SiteTree.Visibility', 'Visibility');
2347
            $labels['LinkChangeNote'] = _t(
2348
                'SiteTree.LINKCHANGENOTE',
2349
                'Changing this page\'s link will also affect the links of all child pages.'
2350
            );
2351
2352
            if ($includerelations) {
2353
                $labels['Parent'] = _t('SiteTree.has_one_Parent', 'Parent Page', 'The parent page in the site hierarchy');
2354
                $labels['LinkTracking'] = _t('SiteTree.many_many_LinkTracking', 'Link Tracking');
2355
                $labels['ImageTracking'] = _t('SiteTree.many_many_ImageTracking', 'Image Tracking');
2356
                $labels['BackLinkTracking'] = _t('SiteTree.many_many_BackLinkTracking', 'Backlink Tracking');
2357
            }
2358
2359
            self::$_cache_field_labels[$cacheKey] = $labels;
2360
        }
2361
2362
        return self::$_cache_field_labels[$cacheKey];
2363
    }
2364
2365
    /**
2366
     * Get the actions available in the CMS for this page - eg Save, Publish.
2367
     *
2368
     * Frontend scripts and styles know how to handle the following FormFields:
2369
     * - top-level FormActions appear as standalone buttons
2370
     * - top-level CompositeField with FormActions within appear as grouped buttons
2371
     * - TabSet & Tabs appear as a drop ups
2372
     * - FormActions within the Tab are restyled as links
2373
     * - major actions can provide alternate states for richer presentation (see ssui.button widget extension)
2374
     *
2375
     * @return FieldList The available actions for this page.
2376
     */
2377
    public function getCMSActions()
2378
    {
2379
        // Get status of page
2380
        $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...
2381
        $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...
2382
        $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...
2383
2384
        // Check permissions
2385
        $canPublish = $this->canPublish();
2386
        $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...
2387
        $canEdit = $this->canEdit();
2388
2389
        // Major actions appear as buttons immediately visible as page actions.
2390
        $majorActions = CompositeField::create()->setName('MajorActions');
2391
        $majorActions->setFieldHolderTemplate(get_class($majorActions) . '_holder_buttongroup');
2392
2393
        // Minor options are hidden behind a drop-up and appear as links (although they are still FormActions).
2394
        $rootTabSet = new TabSet('ActionMenus');
2395
        $moreOptions = new Tab(
2396
            'MoreOptions',
2397
            _t('SiteTree.MoreOptions', 'More options', 'Expands a view for more buttons')
2398
        );
2399
        $rootTabSet->push($moreOptions);
2400
        $rootTabSet->addExtraClass('ss-ui-action-tabset action-menus noborder');
2401
2402
        // Render page information into the "more-options" drop-up, on the top.
2403
        $liveRecord = Versioned::get_by_stage(self::class, Versioned::LIVE)->byID($this->ID);
2404
        $infoTemplate = SSViewer::get_templates_by_class(static::class, '_Information', self::class);
2405
        $moreOptions->push(
2406
            new LiteralField(
2407
                'Information',
2408
                $this->customise(array(
2409
                    'Live' => $liveRecord,
2410
                    'ExistsOnLive' => $isPublished
2411
                ))->renderWith($infoTemplate)
2412
            )
2413
        );
2414
2415
        // Add to campaign option if not-archived and has publish permission
2416
        if (($isPublished || $isOnDraft) && $canPublish) {
2417
            $moreOptions->push(
2418
                AddToCampaignHandler_FormAction::create()
2419
                    ->removeExtraClass('btn-primary')
2420
                    ->addExtraClass('btn-secondary')
2421
            );
2422
        }
2423
2424
        // "readonly"/viewing version that isn't the current version of the record
2425
        $stageRecord = Versioned::get_by_stage(static::class, Versioned::DRAFT)->byID($this->ID);
2426
        /** @skipUpgrade */
2427
        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...
2428
            $moreOptions->push(FormAction::create('email', _t('CMSMain.EMAIL', 'Email')));
2429
            $moreOptions->push(FormAction::create('rollback', _t('CMSMain.ROLLBACK', 'Roll back to this version')));
2430
            $actions = new FieldList(array($majorActions, $rootTabSet));
2431
2432
            // getCMSActions() can be extended with updateCMSActions() on a extension
2433
            $this->extend('updateCMSActions', $actions);
2434
            return $actions;
2435
        }
2436
2437
        // "unpublish"
2438
        if ($isPublished && $canPublish && $isOnDraft && $canUnpublish) {
2439
            $moreOptions->push(
2440
                FormAction::create('unpublish', _t('SiteTree.BUTTONUNPUBLISH', 'Unpublish'), 'delete')
2441
                    ->setDescription(_t('SiteTree.BUTTONUNPUBLISHDESC', 'Remove this page from the published site'))
2442
                    ->addExtraClass('btn-secondary')
2443
            );
2444
        }
2445
2446
        // "rollback"
2447
        if ($isOnDraft && $isPublished && $canEdit && $stagesDiffer) {
2448
            $moreOptions->push(
2449
                FormAction::create('rollback', _t('SiteTree.BUTTONCANCELDRAFT', 'Cancel draft changes'))
2450
                    ->setDescription(_t(
2451
                        'SiteTree.BUTTONCANCELDRAFTDESC',
2452
                        'Delete your draft and revert to the currently published page'
2453
                    ))
2454
                    ->addExtraClass('btn-secondary')
2455
            );
2456
        }
2457
2458
        // "restore"
2459
        if ($canEdit && !$isOnDraft && $isPublished) {
2460
            $majorActions->push(FormAction::create('revert', _t('CMSMain.RESTORE', 'Restore')));
2461
        }
2462
2463
        // Check if we can restore a deleted page
2464
        // Note: It would be nice to have a canRestore() permission at some point
2465
        if ($canEdit && !$isOnDraft && !$isPublished) {
2466
            // Determine if we should force a restore to root (where once it was a subpage)
2467
            $restoreToRoot = $this->isParentArchived();
2468
2469
            // "restore"
2470
            $title = $restoreToRoot
2471
                ? _t('CMSMain.RESTORE_TO_ROOT', 'Restore draft at top level')
2472
                : _t('CMSMain.RESTORE', 'Restore draft');
2473
            $description = $restoreToRoot
2474
                ? _t('CMSMain.RESTORE_TO_ROOT_DESC', 'Restore the archived version to draft as a top level page')
2475
                : _t('CMSMain.RESTORE_DESC', 'Restore the archived version to draft');
2476
            $majorActions->push(
2477
                FormAction::create('restore', $title)
2478
                    ->setDescription($description)
2479
                    ->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...
2480
                    ->setAttribute('data-icon', 'decline')
2481
            );
2482
        }
2483
2484
        // If a page is on any stage it can be archived
2485
        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...
2486
            $title = $isPublished
2487
                ? _t('CMSMain.UNPUBLISH_AND_ARCHIVE', 'Unpublish and archive')
2488
                : _t('CMSMain.ARCHIVE', 'Archive');
2489
            $moreOptions->push(
2490
                FormAction::create('archive', $title)
2491
                    ->addExtraClass('delete btn btn-secondary')
2492
                    ->setDescription(_t(
2493
                        'SiteTree.BUTTONDELETEDESC',
2494
                        'Remove from draft/live and send to archive'
2495
                    ))
2496
            );
2497
        }
2498
2499
        // "save", supports an alternate state that is still clickable, but notifies the user that the action is not needed.
2500
        if ($canEdit && $isOnDraft) {
2501
            $majorActions->push(
2502
                FormAction::create('save', _t('SiteTree.BUTTONSAVED', 'Saved'))
2503
                    ->addExtraClass('btn-secondary-outline font-icon-check-mark')
2504
                    ->setAttribute('data-btn-alternate', 'btn action btn-primary font-icon-save')
2505
                    ->setUseButtonTag(true)
2506
                    ->setAttribute('data-text-alternate', _t('CMSMain.SAVEDRAFT', 'Save draft'))
2507
            );
2508
        }
2509
2510
        if ($canPublish && $isOnDraft) {
2511
            // "publish", as with "save", it supports an alternate state to show when action is needed.
2512
            $majorActions->push(
2513
                $publish = FormAction::create('publish', _t('SiteTree.BUTTONPUBLISHED', 'Published'))
2514
                    ->addExtraClass('btn-secondary-outline font-icon-check-mark')
2515
                    ->setAttribute('data-btn-alternate', 'btn action btn-primary font-icon-rocket')
2516
                    ->setUseButtonTag(true)
2517
                    ->setAttribute('data-text-alternate', _t('SiteTree.BUTTONSAVEPUBLISH', 'Save & publish'))
2518
            );
2519
2520
            // Set up the initial state of the button to reflect the state of the underlying SiteTree object.
2521
            if ($stagesDiffer) {
2522
                $publish->addExtraClass('btn-primary font-icon-rocket');
2523
                $publish->setTitle(_t('SiteTree.BUTTONSAVEPUBLISH', 'Save & publish'));
2524
                $publish->removeExtraClass('btn-secondary-outline font-icon-check-mark');
2525
            }
2526
        }
2527
2528
        $actions = new FieldList(array($majorActions, $rootTabSet));
2529
2530
        // Hook for extensions to add/remove actions.
2531
        $this->extend('updateCMSActions', $actions);
2532
2533
        return $actions;
2534
    }
2535
2536
    public function onAfterPublish()
2537
    {
2538
        // Force live sort order to match stage sort order
2539
        DB::prepared_query(
2540
            'UPDATE "SiteTree_Live"
2541
			SET "Sort" = (SELECT "SiteTree"."Sort" FROM "SiteTree" WHERE "SiteTree_Live"."ID" = "SiteTree"."ID")
2542
			WHERE EXISTS (SELECT "SiteTree"."Sort" FROM "SiteTree" WHERE "SiteTree_Live"."ID" = "SiteTree"."ID") AND "ParentID" = ?',
2543
            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...
2544
        );
2545
    }
2546
2547
    /**
2548
     * Update draft dependant pages
2549
     */
2550
    public function onAfterRevertToLive()
2551
    {
2552
        // Use an alias to get the updates made by $this->publish
2553
        /** @var SiteTree $stageSelf */
2554
        $stageSelf = Versioned::get_by_stage(self::class, Versioned::DRAFT)->byID($this->ID);
2555
        $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...
2556
2557
        // Need to update pages linking to this one as no longer broken
2558
        foreach ($stageSelf->DependentPages() as $page) {
2559
            /** @var SiteTree $page */
2560
            $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...
2561
        }
2562
    }
2563
2564
    /**
2565
     * Determine if this page references a parent which is archived, and not available in stage
2566
     *
2567
     * @return bool True if there is an archived parent
2568
     */
2569
    protected function isParentArchived()
2570
    {
2571
        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...
2572
            /** @var SiteTree $parentPage */
2573
            $parentPage = Versioned::get_latest_version(self::class, $parentID);
2574
            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...
2575
                return true;
2576
            }
2577
        }
2578
        return false;
2579
    }
2580
2581
    /**
2582
     * Restore the content in the active copy of this SiteTree page to the stage site.
2583
     *
2584
     * @return self
2585
     */
2586
    public function doRestoreToStage()
2587
    {
2588
        $this->invokeWithExtensions('onBeforeRestoreToStage', $this);
2589
2590
        // Ensure that the parent page is restored, otherwise restore to root
2591
        if ($this->isParentArchived()) {
2592
            $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...
2593
        }
2594
2595
        // if no record can be found on draft stage (meaning it has been "deleted from draft" before),
2596
        // create an empty record
2597
        if (!DB::prepared_query("SELECT \"ID\" FROM \"SiteTree\" WHERE \"ID\" = ?", array($this->ID))->value()) {
2598
            $conn = DB::get_conn();
2599
            if (method_exists($conn, 'allowPrimaryKeyEditing')) {
2600
                $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...
2601
            }
2602
            DB::prepared_query("INSERT INTO \"SiteTree\" (\"ID\") VALUES (?)", array($this->ID));
2603
            if (method_exists($conn, 'allowPrimaryKeyEditing')) {
2604
                $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...
2605
            }
2606
        }
2607
2608
        $oldReadingMode = Versioned::get_reading_mode();
2609
        Versioned::set_stage(Versioned::DRAFT);
2610
        $this->forceChange();
2611
        $this->write();
2612
2613
        /** @var SiteTree $result */
2614
        $result = DataObject::get_by_id(self::class, $this->ID);
2615
2616
        // Need to update pages linking to this one as no longer broken
2617
        foreach ($result->DependentPages(false) as $page) {
2618
            // $page->write() calls syncLinkTracking, which does all the hard work for us.
2619
            $page->write();
2620
        }
2621
2622
        Versioned::set_reading_mode($oldReadingMode);
2623
2624
        $this->invokeWithExtensions('onAfterRestoreToStage', $this);
2625
2626
        return $result;
2627
    }
2628
2629
    /**
2630
     * Check if this page is new - that is, if it has yet to have been written to the database.
2631
     *
2632
     * @return bool
2633
     */
2634
    public function isNew()
2635
    {
2636
        /**
2637
         * This check was a problem for a self-hosted site, and may indicate a bug in the interpreter on their server,
2638
         * or a bug here. Changing the condition from empty($this->ID) to !$this->ID && !$this->record['ID'] fixed this.
2639
         */
2640
        if (empty($this->ID)) {
2641
            return true;
2642
        }
2643
2644
        if (is_numeric($this->ID)) {
2645
            return false;
2646
        }
2647
2648
        return stripos($this->ID, 'new') === 0;
2649
    }
2650
2651
    /**
2652
     * Get the class dropdown used in the CMS to change the class of a page. This returns the list of options in the
2653
     * dropdown as a Map from class name to singular name. Filters by {@link SiteTree->canCreate()}, as well as
2654
     * {@link SiteTree::$needs_permission}.
2655
     *
2656
     * @return array
2657
     */
2658
    protected function getClassDropdown()
2659
    {
2660
        $classes = self::page_type_classes();
2661
        $currentClass = null;
2662
2663
        $result = array();
2664
        foreach ($classes as $class) {
2665
            $instance = singleton($class);
2666
2667
            // if the current page type is this the same as the class type always show the page type in the list
2668
            if ($this->ClassName != $instance->ClassName) {
2669
                if ($instance instanceof HiddenClass) {
2670
                    continue;
2671
                }
2672
                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...
2673
                    continue;
2674
                }
2675
            }
2676
2677
            if ($perms = $instance->stat('need_permission')) {
2678
                if (!$this->can($perms)) {
2679
                    continue;
2680
                }
2681
            }
2682
2683
            $pageTypeName = $instance->i18n_singular_name();
2684
2685
            $currentClass = $class;
2686
            $result[$class] = $pageTypeName;
2687
2688
            // If we're in translation mode, the link between the translated pagetype title and the actual classname
2689
            // might not be obvious, so we add it in parantheses. Example: class "RedirectorPage" has the title
2690
            // "Weiterleitung" in German, so it shows up as "Weiterleitung (RedirectorPage)"
2691
            if (i18n::get_lang_from_locale(i18n::get_locale()) != 'en') {
2692
                $result[$class] = $result[$class] .  " ({$class})";
2693
            }
2694
        }
2695
2696
        // sort alphabetically, and put current on top
2697
        asort($result);
2698
        if ($currentClass) {
2699
            $currentPageTypeName = $result[$currentClass];
2700
            unset($result[$currentClass]);
2701
            $result = array_reverse($result);
2702
            $result[$currentClass] = $currentPageTypeName;
2703
            $result = array_reverse($result);
2704
        }
2705
2706
        return $result;
2707
    }
2708
2709
    /**
2710
     * Returns an array of the class names of classes that are allowed to be children of this class.
2711
     *
2712
     * @return string[]
2713
     */
2714
    public function allowedChildren()
2715
    {
2716
        $allowedChildren = array();
2717
        $candidates = $this->stat('allowed_children');
2718
        if ($candidates && $candidates != "none" && $candidates != "SiteTree_root") {
2719
            foreach ($candidates as $candidate) {
2720
                // If a classname is prefixed by "*", such as "*Page", then only that class is allowed - no subclasses.
2721
                // Otherwise, the class and all its subclasses are allowed.
2722
                if (substr($candidate, 0, 1) == '*') {
2723
                    $allowedChildren[] = substr($candidate, 1);
2724
                } elseif ($subclasses = ClassInfo::subclassesFor($candidate)) {
2725
                    foreach ($subclasses as $subclass) {
2726
                        if ($subclass == 'SiteTree_root' || singleton($subclass) instanceof HiddenClass) {
2727
                            continue;
2728
                        }
2729
                        $allowedChildren[] = $subclass;
2730
                    }
2731
                }
2732
            }
2733
        }
2734
2735
        return $allowedChildren;
2736
    }
2737
2738
    /**
2739
     * Returns the class name of the default class for children of this page.
2740
     *
2741
     * @return string
2742
     */
2743
    public function defaultChild()
2744
    {
2745
        $default = $this->stat('default_child');
2746
        $allowed = $this->allowedChildren();
2747
        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...
2748
            if (!$default || !in_array($default, $allowed)) {
2749
                $default = reset($allowed);
2750
            }
2751
            return $default;
2752
        }
2753
        return null;
2754
    }
2755
2756
    /**
2757
     * Returns the class name of the default class for the parent of this page.
2758
     *
2759
     * @return string
2760
     */
2761
    public function defaultParent()
2762
    {
2763
        return $this->stat('default_parent');
2764
    }
2765
2766
    /**
2767
     * Get the title for use in menus for this page. If the MenuTitle field is set it returns that, else it returns the
2768
     * Title field.
2769
     *
2770
     * @return string
2771
     */
2772
    public function getMenuTitle()
2773
    {
2774
        if ($value = $this->getField("MenuTitle")) {
2775
            return $value;
2776
        } else {
2777
            return $this->getField("Title");
2778
        }
2779
    }
2780
2781
2782
    /**
2783
     * Set the menu title for this page.
2784
     *
2785
     * @param string $value
2786
     */
2787
    public function setMenuTitle($value)
2788
    {
2789
        if ($value == $this->getField("Title")) {
2790
            $this->setField("MenuTitle", null);
2791
        } else {
2792
            $this->setField("MenuTitle", $value);
2793
        }
2794
    }
2795
2796
    /**
2797
     * A flag provides the user with additional data about the current page status, for example a "removed from draft"
2798
     * status. Each page can have more than one status flag. Returns a map of a unique key to a (localized) title for
2799
     * the flag. The unique key can be reused as a CSS class. Use the 'updateStatusFlags' extension point to customize
2800
     * the flags.
2801
     *
2802
     * Example (simple):
2803
     *   "deletedonlive" => "Deleted"
2804
     *
2805
     * Example (with optional title attribute):
2806
     *   "deletedonlive" => array('text' => "Deleted", 'title' => 'This page has been deleted')
2807
     *
2808
     * @param bool $cached Whether to serve the fields from cache; false regenerate them
2809
     * @return array
2810
     */
2811
    public function getStatusFlags($cached = true)
2812
    {
2813
        if (!$this->_cache_statusFlags || !$cached) {
2814
            $flags = array();
2815
            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...
2816
                $flags['removedfromdraft'] = array(
2817
                    'text' => _t('SiteTree.ONLIVEONLYSHORT', 'On live only'),
2818
                    'title' => _t('SiteTree.ONLIVEONLYSHORTHELP', 'Page is published, but has been deleted from draft'),
2819
                );
2820
            } 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...
2821
                $flags['archived'] = array(
2822
                    'text' => _t('SiteTree.ARCHIVEDPAGESHORT', 'Archived'),
2823
                    'title' => _t('SiteTree.ARCHIVEDPAGEHELP', 'Page is removed from draft and live'),
2824
                );
2825
            } 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...
2826
                $flags['addedtodraft'] = array(
2827
                    'text' => _t('SiteTree.ADDEDTODRAFTSHORT', 'Draft'),
2828
                    'title' => _t('SiteTree.ADDEDTODRAFTHELP', "Page has not been published yet")
2829
                );
2830
            } 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...
2831
                $flags['modified'] = array(
2832
                    'text' => _t('SiteTree.MODIFIEDONDRAFTSHORT', 'Modified'),
2833
                    'title' => _t('SiteTree.MODIFIEDONDRAFTHELP', 'Page has unpublished changes'),
2834
                );
2835
            }
2836
2837
            $this->extend('updateStatusFlags', $flags);
2838
2839
            $this->_cache_statusFlags = $flags;
2840
        }
2841
2842
        return $this->_cache_statusFlags;
2843
    }
2844
2845
    /**
2846
     * getTreeTitle will return three <span> html DOM elements, an empty <span> with the class 'jstree-pageicon' in
2847
     * front, following by a <span> wrapping around its MenutTitle, then following by a <span> indicating its
2848
     * publication status.
2849
     *
2850
     * @return string An HTML string ready to be directly used in a template
2851
     */
2852
    public function getTreeTitle()
2853
    {
2854
        // Build the list of candidate children
2855
        $children = array();
2856
        $candidates = static::page_type_classes();
2857
        foreach ($this->allowedChildren() as $childClass) {
2858
            if (!in_array($childClass, $candidates)) {
2859
                continue;
2860
            }
2861
            $child = singleton($childClass);
2862
            if ($child->canCreate(null, array('Parent' => $this))) {
2863
                $children[$childClass] = $child->i18n_singular_name();
2864
            }
2865
        }
2866
        $flags = $this->getStatusFlags();
2867
        $treeTitle = sprintf(
2868
            "<span class=\"jstree-pageicon\"></span><span class=\"item\" data-allowedchildren=\"%s\">%s</span>",
2869
            Convert::raw2att(Convert::raw2json($children)),
2870
            Convert::raw2xml(str_replace(array("\n","\r"), "", $this->MenuTitle))
2871
        );
2872
        foreach ($flags as $class => $data) {
2873
            if (is_string($data)) {
2874
                $data = array('text' => $data);
2875
            }
2876
            $treeTitle .= sprintf(
2877
                "<span class=\"badge %s\"%s>%s</span>",
2878
                'status-' . Convert::raw2xml($class),
2879
                (isset($data['title'])) ? sprintf(' title="%s"', Convert::raw2xml($data['title'])) : '',
2880
                Convert::raw2xml($data['text'])
2881
            );
2882
        }
2883
2884
        return $treeTitle;
2885
    }
2886
2887
    /**
2888
     * Returns the page in the current page stack of the given level. Level(1) will return the main menu item that
2889
     * we're currently inside, etc.
2890
     *
2891
     * @param int $level
2892
     * @return SiteTree
2893
     */
2894
    public function Level($level)
2895
    {
2896
        $parent = $this;
2897
        $stack = array($parent);
2898
        while (($parent = $parent->Parent()) && $parent->exists()) {
2899
            array_unshift($stack, $parent);
2900
        }
2901
2902
        return isset($stack[$level-1]) ? $stack[$level-1] : null;
2903
    }
2904
2905
    /**
2906
     * Gets the depth of this page in the sitetree, where 1 is the root level
2907
     *
2908
     * @return int
2909
     */
2910
    public function getPageLevel()
2911
    {
2912
        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...
2913
            return 1 + $this->Parent()->getPageLevel();
2914
        }
2915
        return 1;
2916
    }
2917
2918
    /**
2919
     * Find the controller name by our convention of {$ModelClass}Controller
2920
     *
2921
     * @return string
2922
     */
2923
    public function getControllerName()
2924
    {
2925
        //default controller for SiteTree objects
2926
        $controller = ContentController::class;
2927
2928
        //go through the ancestry for this class looking for
2929
        $ancestry = ClassInfo::ancestry(static::class);
2930
        // loop over the array going from the deepest descendant (ie: the current class) to SiteTree
2931
        while ($class = array_pop($ancestry)) {
2932
            //we don't need to go any deeper than the SiteTree class
2933
            if ($class == SiteTree::class) {
2934
                break;
2935
            }
2936
            // If we have a class of "{$ClassName}Controller" then we found our controller
2937
            if (class_exists($candidate = sprintf('%sController', $class))) {
2938
                $controller = $candidate;
2939
                break;
2940
            } elseif (class_exists($candidate = sprintf('%s_Controller', $class))) {
2941
                // Support the legacy underscored filename, but raise a deprecation notice
2942
                Deprecation::notice(
2943
                    '5.0',
2944
                    'Underscored controller class names are deprecated. Use "MyController" instead of "My_Controller".',
2945
                    Deprecation::SCOPE_GLOBAL
2946
                );
2947
                $controller = $candidate;
2948
                break;
2949
            }
2950
        }
2951
2952
        return $controller;
2953
    }
2954
2955
    /**
2956
     * Return the CSS classes to apply to this node in the CMS tree.
2957
     *
2958
     * @param string $numChildrenMethod
2959
     * @return string
2960
     */
2961
    public function CMSTreeClasses($numChildrenMethod = "numChildren")
2962
    {
2963
        $classes = sprintf('class-%s', static::class);
2964
        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...
2965
            $classes .= " BrokenLink";
2966
        }
2967
2968
        if (!$this->canAddChildren()) {
2969
            $classes .= " nochildren";
2970
        }
2971
2972
        if (!$this->canEdit() && !$this->canAddChildren()) {
2973
            if (!$this->canView()) {
2974
                $classes .= " disabled";
2975
            } else {
2976
                $classes .= " edit-disabled";
2977
            }
2978
        }
2979
2980
        if (!$this->ShowInMenus) {
2981
            $classes .= " notinmenu";
2982
        }
2983
2984
        //TODO: Add integration
2985
        /*
2986
		if($this->hasExtension('Translatable') && $controller->Locale != Translatable::default_locale() && !$this->isTranslation())
2987
			$classes .= " untranslated ";
2988
		*/
2989
        $classes .= $this->markingClasses($numChildrenMethod);
0 ignored issues
show
Documentation Bug introduced by
The method markingClasses does not exist on object<SilverStripe\CMS\Model\SiteTree>? Since you implemented __call, maybe consider adding a @method annotation.

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

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

class ParentClass {
    private $data = array();

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

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

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
2990
2991
        return $classes;
2992
    }
2993
2994
    /**
2995
     * Stops extendCMSFields() being called on getCMSFields(). This is useful when you need access to fields added by
2996
     * subclasses of SiteTree in a extension. Call before calling parent::getCMSFields(), and reenable afterwards.
2997
     */
2998
    public static function disableCMSFieldsExtensions()
2999
    {
3000
        self::$runCMSFieldsExtensions = false;
3001
    }
3002
3003
    /**
3004
     * Reenables extendCMSFields() being called on getCMSFields() after it has been disabled by
3005
     * disableCMSFieldsExtensions().
3006
     */
3007
    public static function enableCMSFieldsExtensions()
3008
    {
3009
        self::$runCMSFieldsExtensions = true;
3010
    }
3011
3012
    public function providePermissions()
3013
    {
3014
        return array(
3015
            'SITETREE_GRANT_ACCESS' => array(
3016
                'name' => _t('SiteTree.PERMISSION_GRANTACCESS_DESCRIPTION', 'Manage access rights for content'),
3017
                'help' => _t('SiteTree.PERMISSION_GRANTACCESS_HELP', 'Allow setting of page-specific access restrictions in the "Pages" section.'),
3018
                'category' => _t('Permissions.PERMISSIONS_CATEGORY', 'Roles and access permissions'),
3019
                'sort' => 100
3020
            ),
3021
            'SITETREE_VIEW_ALL' => array(
3022
                'name' => _t('SiteTree.VIEW_ALL_DESCRIPTION', 'View any page'),
3023
                'category' => _t('Permissions.CONTENT_CATEGORY', 'Content permissions'),
3024
                'sort' => -100,
3025
                'help' => _t('SiteTree.VIEW_ALL_HELP', 'Ability to view any page on the site, regardless of the settings on the Access tab.  Requires the "Access to \'Pages\' section" permission')
3026
            ),
3027
            'SITETREE_EDIT_ALL' => array(
3028
                'name' => _t('SiteTree.EDIT_ALL_DESCRIPTION', 'Edit any page'),
3029
                'category' => _t('Permissions.CONTENT_CATEGORY', 'Content permissions'),
3030
                'sort' => -50,
3031
                'help' => _t('SiteTree.EDIT_ALL_HELP', 'Ability to edit any page on the site, regardless of the settings on the Access tab.  Requires the "Access to \'Pages\' section" permission')
3032
            ),
3033
            'SITETREE_REORGANISE' => array(
3034
                'name' => _t('SiteTree.REORGANISE_DESCRIPTION', 'Change site structure'),
3035
                'category' => _t('Permissions.CONTENT_CATEGORY', 'Content permissions'),
3036
                'help' => _t('SiteTree.REORGANISE_HELP', 'Rearrange pages in the site tree through drag&drop.'),
3037
                'sort' => 100
3038
            ),
3039
            'VIEW_DRAFT_CONTENT' => array(
3040
                'name' => _t('SiteTree.VIEW_DRAFT_CONTENT', 'View draft content'),
3041
                'category' => _t('Permissions.CONTENT_CATEGORY', 'Content permissions'),
3042
                'help' => _t('SiteTree.VIEW_DRAFT_CONTENT_HELP', 'Applies to viewing pages outside of the CMS in draft mode. Useful for external collaborators without CMS access.'),
3043
                'sort' => 100
3044
            )
3045
        );
3046
    }
3047
3048
    /**
3049
     * Default singular name for page / sitetree
3050
     *
3051
     * @return string
3052
     */
3053 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...
3054
    {
3055
        $base = in_array(static::class, [Page::class, self::class]);
3056
        if ($base) {
3057
            return $this->stat('base_singular_name');
3058
        }
3059
        return parent::singular_name();
3060
    }
3061
3062
    /**
3063
     * Default plural name for page / sitetree
3064
     *
3065
     * @return string
3066
     */
3067 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...
3068
    {
3069
        $base = in_array(static::class, [Page::class, self::class]);
3070
        if ($base) {
3071
            return $this->stat('base_plural_name');
3072
        }
3073
        return parent::plural_name();
3074
    }
3075
3076
    /**
3077
     * Get description for this page
3078
     *
3079
     * @return string|null
3080
     */
3081
    public function description()
3082
    {
3083
        $base = in_array(static::class, [Page::class, self::class]);
3084
        if ($base) {
3085
            return $this->stat('base_description');
3086
        }
3087
        return $this->stat('description');
3088
    }
3089
3090
    /**
3091
     * Get localised description for this page
3092
     *
3093
     * @return string|null
3094
     */
3095
    public function i18n_description()
3096
    {
3097
        $description = $this->description();
3098
        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...
3099
            return _t(static::class.'.DESCRIPTION', $description);
3100
        }
3101
        return null;
3102
    }
3103
3104
    /**
3105
     * Overloaded to also provide entities for 'Page' class which is usually located in custom code, hence textcollector
3106
     * picks it up for the wrong folder.
3107
     *
3108
     * @return array
3109
     */
3110
    public function provideI18nEntities()
3111
    {
3112
        $entities = parent::provideI18nEntities();
3113
3114
        // Add optional description
3115
        $description = $this->description();
3116
        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...
3117
            $entities[static::class . '.DESCRIPTION'] = $description;
3118
        }
3119
        return $entities;
0 ignored issues
show
Best Practice introduced by
The expression return $entities; seems to be an array, but some of its elements' types (string) are incompatible with the return type of the parent method SilverStripe\ORM\DataObject::provideI18nEntities of type array<*,string[]>.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
3120
    }
3121
3122
    /**
3123
     * Returns 'root' if the current page has no parent, or 'subpage' otherwise
3124
     *
3125
     * @return string
3126
     */
3127
    public function getParentType()
3128
    {
3129
        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...
3130
    }
3131
3132
    /**
3133
     * Clear the permissions cache for SiteTree
3134
     */
3135
    public static function reset()
3136
    {
3137
        self::$cache_permissions = array();
3138
    }
3139
3140
    public static function on_db_reset()
3141
    {
3142
        self::$cache_permissions = array();
3143
    }
3144
}
3145