Completed
Pull Request — master (#1724)
by Damian
02:26
created

SiteTree::collateDescendants()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 15
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

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

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

<?php

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

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

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

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

}

Since the property has write access only, you can use the @property-write annotation instead.

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

See also the PhpDoc documentation for @property.

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