Completed
Pull Request — master (#1769)
by Damian
02:20
created

SiteTree::classDescription()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 8
rs 9.4285
c 0
b 0
f 0
cc 2
eloc 5
nc 2
nop 0
1
<?php
2
3
namespace SilverStripe\CMS\Model;
4
5
use Page;
6
use SilverStripe\CampaignAdmin\AddToCampaignHandler_FormAction;
7
use SilverStripe\ORM\CMSPreviewable;
8
use SilverStripe\CMS\Controllers\CMSPageEditController;
9
use SilverStripe\CMS\Controllers\ContentController;
10
use SilverStripe\CMS\Controllers\ModelAsController;
11
use SilverStripe\CMS\Controllers\RootURLController;
12
use SilverStripe\CMS\Forms\SiteTreeURLSegmentField;
13
use SilverStripe\Control\ContentNegotiator;
14
use SilverStripe\Control\Controller;
15
use SilverStripe\Control\Director;
16
use SilverStripe\Control\RequestHandler;
17
use SilverStripe\Core\ClassInfo;
18
use SilverStripe\Core\Config\Config;
19
use SilverStripe\Core\Convert;
20
use SilverStripe\Core\Resettable;
21
use SilverStripe\Dev\Deprecation;
22
use SilverStripe\Forms\CheckboxField;
23
use SilverStripe\Forms\CompositeField;
24
use SilverStripe\Forms\DropdownField;
25
use SilverStripe\Forms\FieldGroup;
26
use SilverStripe\Forms\FieldList;
27
use SilverStripe\Forms\FormAction;
28
use SilverStripe\Forms\FormField;
29
use SilverStripe\Forms\GridField\GridField;
30
use SilverStripe\Forms\GridField\GridFieldDataColumns;
31
use SilverStripe\Forms\HTMLEditor\HTMLEditorField;
32
use SilverStripe\Forms\ListboxField;
33
use SilverStripe\Forms\LiteralField;
34
use SilverStripe\Forms\OptionsetField;
35
use SilverStripe\Forms\Tab;
36
use SilverStripe\Forms\TabSet;
37
use SilverStripe\Forms\TextareaField;
38
use SilverStripe\Forms\TextField;
39
use SilverStripe\Forms\ToggleCompositeField;
40
use SilverStripe\Forms\TreeDropdownField;
41
use SilverStripe\i18n\i18n;
42
use SilverStripe\i18n\i18nEntityProvider;
43
use SilverStripe\ORM\ArrayList;
44
use SilverStripe\ORM\DataList;
45
use SilverStripe\ORM\DataObject;
46
use SilverStripe\ORM\DB;
47
use SilverStripe\ORM\HiddenClass;
48
use SilverStripe\ORM\Hierarchy\Hierarchy;
49
use SilverStripe\ORM\ManyManyList;
50
use SilverStripe\ORM\ValidationResult;
51
use SilverStripe\Versioned\Versioned;
52
use SilverStripe\Security\Group;
53
use SilverStripe\Security\Member;
54
use SilverStripe\Security\Permission;
55
use SilverStripe\Security\PermissionProvider;
56
use SilverStripe\SiteConfig\SiteConfig;
57
use SilverStripe\View\ArrayData;
58
use SilverStripe\View\Parsers\ShortcodeParser;
59
use SilverStripe\View\Parsers\URLSegmentFilter;
60
use SilverStripe\View\SSViewer;
61
use Subsite;
62
63
/**
64
 * Basic data-object representing all pages within the site tree. All page types that live within the hierarchy should
65
 * inherit from this. In addition, it contains a number of static methods for querying the site tree and working with
66
 * draft and published states.
67
 *
68
 * <h2>URLs</h2>
69
 * A page is identified during request handling via its "URLSegment" database column. As pages can be nested, the full
70
 * path of a URL might contain multiple segments. Each segment is stored in its filtered representation (through
71
 * {@link URLSegmentFilter}). The full path is constructed via {@link Link()}, {@link RelativeLink()} and
72
 * {@link AbsoluteLink()}. You can allow these segments to contain multibyte characters through
73
 * {@link URLSegmentFilter::$default_allow_multibyte}.
74
 *
75
 * @property string URLSegment
76
 * @property string Title
77
 * @property string MenuTitle
78
 * @property string Content HTML content of the page.
79
 * @property string MetaDescription
80
 * @property string ExtraMeta
81
 * @property string ShowInMenus
82
 * @property string ShowInSearch
83
 * @property string Sort Integer value denoting the sort order.
84
 * @property string ReportClass
85
 * @property string CanViewType Type of restriction for viewing this object.
86
 * @property string CanEditType Type of restriction for editing this object.
87
 *
88
 * @method ManyManyList ViewerGroups() List of groups that can view this object.
89
 * @method ManyManyList EditorGroups() List of groups that can edit this object.
90
 * @method SiteTree Parent()
91
 *
92
 * @mixin Hierarchy
93
 * @mixin Versioned
94
 * @mixin SiteTreeLinkTracking
95
 */
96
class SiteTree extends DataObject implements PermissionProvider, i18nEntityProvider, CMSPreviewable, Resettable
97
{
98
99
    /**
100
     * Indicates what kind of children this page type can have.
101
     * This can be an array of allowed child classes, or the string "none" -
102
     * indicating that this page type can't have children.
103
     * If a classname is prefixed by "*", such as "*Page", then only that
104
     * class is allowed - no subclasses. Otherwise, the class and all its
105
     * subclasses are allowed.
106
     * To control allowed children on root level (no parent), use {@link $can_be_root}.
107
     *
108
     * Note that this setting is cached when used in the CMS, use the "flush" query parameter to clear it.
109
     *
110
     * @config
111
     * @var array
112
     */
113
    private static $allowed_children = [
114
        self::class
115
    ];
116
117
    /**
118
     * The default child class for this page.
119
     * Note: Value might be cached, see {@link $allowed_chilren}.
120
     *
121
     * @config
122
     * @var string
123
     */
124
    private static $default_child = "Page";
125
126
    /**
127
     * Default value for SiteTree.ClassName enum
128
     * {@see DBClassName::getDefault}
129
     *
130
     * @config
131
     * @var string
132
     */
133
    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...
134
135
    /**
136
     * The default parent class for this page.
137
     * Note: Value might be cached, see {@link $allowed_chilren}.
138
     *
139
     * @config
140
     * @var string
141
     */
142
    private static $default_parent = null;
143
144
    /**
145
     * Controls whether a page can be in the root of the site tree.
146
     * Note: Value might be cached, see {@link $allowed_chilren}.
147
     *
148
     * @config
149
     * @var bool
150
     */
151
    private static $can_be_root = true;
152
153
    /**
154
     * List of permission codes a user can have to allow a user to create a page of this type.
155
     * Note: Value might be cached, see {@link $allowed_chilren}.
156
     *
157
     * @config
158
     * @var array
159
     */
160
    private static $need_permission = null;
161
162
    /**
163
     * If you extend a class, and don't want to be able to select the old class
164
     * in the cms, set this to the old class name. Eg, if you extended Product
165
     * to make ImprovedProduct, then you would set $hide_ancestor to Product.
166
     *
167
     * @config
168
     * @var string
169
     */
170
    private static $hide_ancestor = null;
171
172
    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...
173
        "URLSegment" => "Varchar(255)",
174
        "Title" => "Varchar(255)",
175
        "MenuTitle" => "Varchar(100)",
176
        "Content" => "HTMLText",
177
        "MetaDescription" => "Text",
178
        "ExtraMeta" => "HTMLFragment(['whitelist' => ['meta', 'link']])",
179
        "ShowInMenus" => "Boolean",
180
        "ShowInSearch" => "Boolean",
181
        "Sort" => "Int",
182
        "HasBrokenFile" => "Boolean",
183
        "HasBrokenLink" => "Boolean",
184
        "ReportClass" => "Varchar",
185
        "CanViewType" => "Enum('Anyone, LoggedInUsers, OnlyTheseUsers, Inherit', 'Inherit')",
186
        "CanEditType" => "Enum('LoggedInUsers, OnlyTheseUsers, Inherit', 'Inherit')",
187
    );
188
189
    private static $indexes = array(
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
190
        "URLSegment" => true,
191
    );
192
193
    private static $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...
194
        "ViewerGroups" => Group::class,
195
        "EditorGroups" => Group::class,
196
    );
197
198
    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...
199
        "VirtualPages" => "SilverStripe\\CMS\\Model\\VirtualPage.CopyContentFrom"
200
    );
201
202
    private static $owned_by = array(
203
        "VirtualPages"
204
    );
205
206
    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...
207
        "Breadcrumbs" => "HTMLFragment",
208
        "LastEdited" => "Datetime",
209
        "Created" => "Datetime",
210
        'Link' => 'Text',
211
        'RelativeLink' => 'Text',
212
        'AbsoluteLink' => 'Text',
213
        'CMSEditLink' => 'Text',
214
        'TreeTitle' => 'HTMLFragment',
215
        'MetaTags' => 'HTMLFragment',
216
    );
217
218
    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...
219
        "ShowInMenus" => 1,
220
        "ShowInSearch" => 1,
221
        "CanViewType" => "Inherit",
222
        "CanEditType" => "Inherit"
223
    );
224
225
    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...
226
227
    private static $versioning = array(
228
        "Stage",  "Live"
229
    );
230
231
    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...
232
233
    /**
234
     * If this is false, the class cannot be created in the CMS by regular content authors, only by ADMINs.
235
     * @var boolean
236
     * @config
237
     */
238
    private static $can_create = true;
239
240
    /**
241
     * Icon to use in the CMS page tree. This should be the full filename, relative to the webroot.
242
     * Also supports custom CSS rule contents (applied to the correct selector for the tree UI implementation).
243
     *
244
     * @see CMSMain::generateTreeStylingCSS()
245
     * @config
246
     * @var string
247
     */
248
    private static $icon = null;
249
250
    private static $extensions = [
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
251
        Hierarchy::class,
252
        Versioned::class,
253
        SiteTreeLinkTracking::class,
254
    ];
255
256
    private static $searchable_fields = array(
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
257
        'Title',
258
        'Content',
259
    );
260
261
    private static $field_labels = array(
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
262
        'URLSegment' => 'URL'
263
    );
264
265
    /**
266
     * @config
267
     */
268
    private static $nested_urls = true;
269
270
    /**
271
     * @config
272
    */
273
    private static $create_default_pages = true;
274
275
    /**
276
     * This controls whether of not extendCMSFields() is called by getCMSFields.
277
     */
278
    private static $runCMSFieldsExtensions = true;
279
280
    /**
281
     * Cache for canView/Edit/Publish/Delete permissions.
282
     * Keyed by permission type (e.g. 'edit'), with an array
283
     * of IDs mapped to their boolean permission ability (true=allow, false=deny).
284
     * See {@link batch_permission_check()} for details.
285
     */
286
    private static $cache_permissions = array();
287
288
    /**
289
     * @config
290
     * @var boolean
291
     */
292
    private static $enforce_strict_hierarchy = true;
293
294
    /**
295
     * The value used for the meta generator tag. Leave blank to omit the tag.
296
     *
297
     * @config
298
     * @var string
299
     */
300
    private static $meta_generator = 'SilverStripe - http://silverstripe.org';
301
302
    protected $_cache_statusFlags = null;
303
304
    /**
305
     * Plural form for SiteTree / Page classes. Not inherited by subclasses.
306
     *
307
     * @config
308
     * @var string
309
     */
310
    private static $base_plural_name = 'Pages';
311
312
    /**
313
     * Plural form for SiteTree / Page classes. Not inherited by subclasses.
314
     *
315
     * @config
316
     * @var string
317
     */
318
    private static $base_singular_name = 'Page';
319
320
    /**
321
     * Description of the class functionality, typically shown to a user
322
     * when selecting which page type to create. Translated through {@link provideI18nEntities()}.
323
     *
324
     * @see SiteTree::classDescription()
325
     * @see SiteTree::i18n_classDescription()
326
     *
327
     * @config
328
     * @var string
329
     */
330
    private static $description = null;
331
332
    /**
333
     * Description for Page and SiteTree classes, but not inherited by subclasses.
334
     * override SiteTree::$description in subclasses instead.
335
     *
336
     * @see SiteTree::classDescription()
337
     * @see SiteTree::i18n_classDescription()
338
     *
339
     * @config
340
     * @var string
341
     */
342
    private static $base_description = 'Generic content page';
343
344
    /**
345
     * Fetches the {@link SiteTree} object that maps to a link.
346
     *
347
     * If you have enabled {@link SiteTree::config()->nested_urls} on this site, then you can use a nested link such as
348
     * "about-us/staff/", and this function will traverse down the URL chain and grab the appropriate link.
349
     *
350
     * Note that if no model can be found, this method will fall over to a extended alternateGetByLink method provided
351
     * by a extension attached to {@link SiteTree}
352
     *
353
     * @param string $link  The link of the page to search for
354
     * @param bool   $cache True (default) to use caching, false to force a fresh search from the database
355
     * @return SiteTree
356
     */
357
    public static function get_by_link($link, $cache = true)
358
    {
359
        if (trim($link, '/')) {
360
            $link = trim(Director::makeRelative($link), '/');
361
        } else {
362
            $link = RootURLController::get_homepage_link();
363
        }
364
365
        $parts = preg_split('|/+|', $link);
366
367
        // Grab the initial root level page to traverse down from.
368
        $URLSegment = array_shift($parts);
369
        $conditions = array('"SiteTree"."URLSegment"' => rawurlencode($URLSegment));
370
        if (self::config()->nested_urls) {
371
            $conditions[] = array('"SiteTree"."ParentID"' => 0);
372
        }
373
        /** @var SiteTree $sitetree */
374
        $sitetree = DataObject::get_one(self::class, $conditions, $cache);
375
376
        /// Fall back on a unique URLSegment for b/c.
377
        if (!$sitetree
378
            && self::config()->nested_urls
379
            && $sitetree = DataObject::get_one(self::class, array(
380
                '"SiteTree"."URLSegment"' => $URLSegment
381
            ), $cache)
382
        ) {
383
            return $sitetree;
384
        }
385
386
        // Attempt to grab an alternative page from extensions.
387
        if (!$sitetree) {
388
            $parentID = self::config()->nested_urls ? 0 : null;
389
390 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...
391
                foreach ($alternatives as $alternative) {
392
                    if ($alternative) {
393
                        $sitetree = $alternative;
394
                    }
395
                }
396
            }
397
398
            if (!$sitetree) {
399
                return null;
400
            }
401
        }
402
403
        // Check if we have any more URL parts to parse.
404
        if (!self::config()->nested_urls || !count($parts)) {
405
            return $sitetree;
406
        }
407
408
        // Traverse down the remaining URL segments and grab the relevant SiteTree objects.
409
        foreach ($parts as $segment) {
410
            $next = DataObject::get_one(
411
                self::class,
412
                array(
413
                    '"SiteTree"."URLSegment"' => $segment,
414
                    '"SiteTree"."ParentID"' => $sitetree->ID
415
                ),
416
                $cache
417
            );
418
419
            if (!$next) {
420
                $parentID = (int) $sitetree->ID;
421
422 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...
423
                    foreach ($alternatives as $alternative) {
424
                        if ($alternative) {
425
                            $next = $alternative;
426
                        }
427
                    }
428
                }
429
430
                if (!$next) {
431
                    return null;
432
                }
433
            }
434
435
            $sitetree->destroy();
436
            $sitetree = $next;
437
        }
438
439
        return $sitetree;
440
    }
441
442
    /**
443
     * Return a subclass map of SiteTree that shouldn't be hidden through {@link SiteTree::$hide_ancestor}
444
     *
445
     * @return array
446
     */
447
    public static function page_type_classes()
448
    {
449
        $classes = ClassInfo::getValidSubClasses();
450
451
        $baseClassIndex = array_search(self::class, $classes);
452
        if ($baseClassIndex !== false) {
453
            unset($classes[$baseClassIndex]);
454
        }
455
456
        $kill_ancestors = array();
457
458
        // figure out if there are any classes we don't want to appear
459
        foreach ($classes as $class) {
460
            $instance = singleton($class);
461
462
            // do any of the progeny want to hide an ancestor?
463
            if ($ancestor_to_hide = $instance->stat('hide_ancestor')) {
464
                // note for killing later
465
                $kill_ancestors[] = $ancestor_to_hide;
466
            }
467
        }
468
469
        // If any of the descendents don't want any of the elders to show up, cruelly render the elders surplus to
470
        // requirements
471
        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...
472
            $kill_ancestors = array_unique($kill_ancestors);
473
            foreach ($kill_ancestors as $mark) {
474
                // unset from $classes
475
                $idx = array_search($mark, $classes, true);
476
                if ($idx !== false) {
477
                    unset($classes[$idx]);
478
                }
479
            }
480
        }
481
482
        return $classes;
483
    }
484
485
    /**
486
     * Replace a "[sitetree_link id=n]" shortcode with a link to the page with the corresponding ID.
487
     *
488
     * @param array      $arguments
489
     * @param string     $content
490
     * @param ShortcodeParser $parser
491
     * @return string
492
     */
493
    public static function link_shortcode_handler($arguments, $content = null, $parser = null)
494
    {
495
        if (!isset($arguments['id']) || !is_numeric($arguments['id'])) {
496
            return null;
497
        }
498
499
        /** @var SiteTree $page */
500
        if (!($page = DataObject::get_by_id(self::class, $arguments['id']))         // Get the current page by ID.
501
            && !($page = Versioned::get_latest_version(self::class, $arguments['id'])) // Attempt link to old version.
502
        ) {
503
             return null; // There were no suitable matches at all.
504
        }
505
506
        /** @var SiteTree $page */
507
        $link = Convert::raw2att($page->Link());
508
509
        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...
510
            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...
511
        } else {
512
            return $link;
513
        }
514
    }
515
516
    /**
517
     * Return the link for this {@link SiteTree} object, with the {@link Director::baseURL()} included.
518
     *
519
     * @param string $action Optional controller action (method).
520
     *                       Note: URI encoding of this parameter is applied automatically through template casting,
521
     *                       don't encode the passed parameter. Please use {@link Controller::join_links()} instead to
522
     *                       append GET parameters.
523
     * @return string
524
     */
525
    public function Link($action = null)
526
    {
527
        return Controller::join_links(Director::baseURL(), $this->RelativeLink($action));
528
    }
529
530
    /**
531
     * Get the absolute URL for this page, including protocol and host.
532
     *
533
     * @param string $action See {@link Link()}
534
     * @return string
535
     */
536
    public function AbsoluteLink($action = null)
537
    {
538
        if ($this->hasMethod('alternateAbsoluteLink')) {
539
            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...
540
        } else {
541
            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 541 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...
542
        }
543
    }
544
545
    /**
546
     * Base link used for previewing. Defaults to absolute URL, in order to account for domain changes, e.g. on multi
547
     * site setups. Does not contain hints about the stage, see {@link SilverStripeNavigator} for details.
548
     *
549
     * @param string $action See {@link Link()}
550
     * @return string
551
     */
552
    public function PreviewLink($action = null)
553
    {
554
        if ($this->hasMethod('alternatePreviewLink')) {
555
            Deprecation::notice('5.0', 'Use updatePreviewLink or override PreviewLink method');
556
            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...
557
        }
558
559
        $link = $this->AbsoluteLink($action);
560
        $this->extend('updatePreviewLink', $link, $action);
561
        return $link;
562
    }
563
564
    public function getMimeType()
565
    {
566
        return 'text/html';
567
    }
568
569
    /**
570
     * Return the link for this {@link SiteTree} object relative to the SilverStripe root.
571
     *
572
     * By default, if this page is the current home page, and there is no action specified then this will return a link
573
     * to the root of the site. However, if you set the $action parameter to TRUE then the link will not be rewritten
574
     * and returned in its full form.
575
     *
576
     * @uses RootURLController::get_homepage_link()
577
     *
578
     * @param string $action See {@link Link()}
579
     * @return string
580
     */
581
    public function RelativeLink($action = null)
582
    {
583
        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...
584
            $parent = $this->Parent();
585
            // If page is removed select parent from version history (for archive page view)
586
            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...
587
                $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...
588
            }
589
            $base = $parent->RelativeLink($this->URLSegment);
590
        } 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...
591
            // Unset base for root-level homepages.
592
            // Note: Homepages with action parameters (or $action === true)
593
            // need to retain their URLSegment.
594
            $base = null;
595
        } else {
596
            $base = $this->URLSegment;
597
        }
598
599
        $this->extend('updateRelativeLink', $base, $action);
600
601
        // Legacy support: If $action === true, retain URLSegment for homepages,
602
        // but don't append any action
603
        if ($action === true) {
604
            $action = null;
605
        }
606
607
        return Controller::join_links($base, '/', $action);
608
    }
609
610
    /**
611
     * Get the absolute URL for this page on the Live site.
612
     *
613
     * @param bool $includeStageEqualsLive Whether to append the URL with ?stage=Live to force Live mode
614
     * @return string
615
     */
616
    public function getAbsoluteLiveLink($includeStageEqualsLive = true)
617
    {
618
        $oldReadingMode = Versioned::get_reading_mode();
619
        Versioned::set_stage(Versioned::LIVE);
620
        /** @var SiteTree $live */
621
        $live = Versioned::get_one_by_stage(self::class, Versioned::LIVE, array(
622
            '"SiteTree"."ID"' => $this->ID
623
        ));
624
        if ($live) {
625
            $link = $live->AbsoluteLink();
626
            if ($includeStageEqualsLive) {
627
                $link = Controller::join_links($link, '?stage=Live');
628
            }
629
        } else {
630
            $link = null;
631
        }
632
633
        Versioned::set_reading_mode($oldReadingMode);
634
        return $link;
635
    }
636
637
    /**
638
     * Generates a link to edit this page in the CMS.
639
     *
640
     * @return string
641
     */
642
    public function CMSEditLink()
643
    {
644
        $link = Controller::join_links(
645
            CMSPageEditController::singleton()->Link('show'),
646
            $this->ID
647
        );
648
        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 648 which is incompatible with the return type declared by the interface SilverStripe\ORM\CMSPreviewable::CMSEditLink of type string. It seems like you forgot to handle an error condition.
Loading history...
649
    }
650
651
652
    /**
653
     * Return a CSS identifier generated from this page's link.
654
     *
655
     * @return string The URL segment
656
     */
657
    public function ElementName()
658
    {
659
        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...
660
    }
661
662
    /**
663
     * Returns true if this is the currently active page being used to handle this request.
664
     *
665
     * @return bool
666
     */
667
    public function isCurrent()
668
    {
669
        $currentPage = Director::get_current_page();
670
        if ($currentPage instanceof ContentController) {
671
            $currentPage = $currentPage->data();
672
        }
673
        if ($currentPage instanceof SiteTree) {
674
            return $currentPage === $this || $currentPage->ID === $this->ID;
675
        }
676
        return false;
677
    }
678
679
    /**
680
     * Check if this page is in the currently active section (e.g. it is either current or one of its children is
681
     * currently being viewed).
682
     *
683
     * @return bool
684
     */
685
    public function isSection()
686
    {
687
        return $this->isCurrent() || (
688
            Director::get_current_page() instanceof SiteTree && in_array($this->ID, Director::get_current_page()->getAncestors()->column())
689
        );
690
    }
691
692
    /**
693
     * Check if the parent of this page has been removed (or made otherwise unavailable), and is still referenced by
694
     * this child. Any such orphaned page may still require access via the CMS, but should not be shown as accessible
695
     * to external users.
696
     *
697
     * @return bool
698
     */
699
    public function isOrphaned()
700
    {
701
        // Always false for root pages
702
        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...
703
            return false;
704
        }
705
706
        // Parent must exist and not be an orphan itself
707
        $parent = $this->Parent();
708
        return !$parent || !$parent->exists() || $parent->isOrphaned();
709
    }
710
711
    /**
712
     * Return "link" or "current" depending on if this is the {@link SiteTree::isCurrent()} current page.
713
     *
714
     * @return string
715
     */
716
    public function LinkOrCurrent()
717
    {
718
        return $this->isCurrent() ? 'current' : 'link';
719
    }
720
721
    /**
722
     * Return "link" or "section" depending on if this is the {@link SiteTree::isSeciton()} current section.
723
     *
724
     * @return string
725
     */
726
    public function LinkOrSection()
727
    {
728
        return $this->isSection() ? 'section' : 'link';
729
    }
730
731
    /**
732
     * Return "link", "current" or "section" depending on if this page is the current page, or not on the current page
733
     * but in the current section.
734
     *
735
     * @return string
736
     */
737
    public function LinkingMode()
738
    {
739
        if ($this->isCurrent()) {
740
            return 'current';
741
        } elseif ($this->isSection()) {
742
            return 'section';
743
        } else {
744
            return 'link';
745
        }
746
    }
747
748
    /**
749
     * Check if this page is in the given current section.
750
     *
751
     * @param string $sectionName Name of the section to check
752
     * @return bool True if we are in the given section
753
     */
754
    public function InSection($sectionName)
755
    {
756
        $page = Director::get_current_page();
757
        while ($page && $page->exists()) {
758
            if ($sectionName == $page->URLSegment) {
759
                return true;
760
            }
761
            $page = $page->Parent();
762
        }
763
        return false;
764
    }
765
766
    /**
767
     * Reset Sort on duped page
768
     *
769
     * @param SiteTree $original
770
     * @param bool $doWrite
771
     */
772
    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...
773
    {
774
        $this->Sort = 0;
775
    }
776
777
    /**
778
     * Duplicates each child of this node recursively and returns the top-level duplicate node.
779
     *
780
     * @return static The duplicated object
781
     */
782
    public function duplicateWithChildren()
783
    {
784
        /** @var SiteTree $clone */
785
        $clone = $this->duplicate();
786
        $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...
787
788
        if ($children) {
789
            /** @var SiteTree $child */
790
            $sort = 0;
791
            foreach ($children as $child) {
792
                $childClone = $child->duplicateWithChildren();
793
                $childClone->ParentID = $clone->ID;
794
                //retain sort order by manually setting sort values
795
                $childClone->Sort = ++$sort;
796
                $childClone->write();
797
            }
798
        }
799
800
        return $clone;
801
    }
802
803
    /**
804
     * Duplicate this node and its children as a child of the node with the given ID
805
     *
806
     * @param int $id ID of the new node's new parent
807
     */
808
    public function duplicateAsChild($id)
809
    {
810
        /** @var SiteTree $newSiteTree */
811
        $newSiteTree = $this->duplicate();
812
        $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...
813
        $newSiteTree->Sort = 0;
814
        $newSiteTree->write();
815
    }
816
817
    /**
818
     * Return a breadcrumb trail to this page. Excludes "hidden" pages (with ShowInMenus=0) by default.
819
     *
820
     * @param int $maxDepth The maximum depth to traverse.
821
     * @param boolean $unlinked Whether to link page titles.
822
     * @param boolean|string $stopAtPageType ClassName of a page to stop the upwards traversal.
823
     * @param boolean $showHidden Include pages marked with the attribute ShowInMenus = 0
824
     * @return string The breadcrumb trail.
825
     */
826
    public function Breadcrumbs($maxDepth = 20, $unlinked = false, $stopAtPageType = false, $showHidden = false)
827
    {
828
        $pages = $this->getBreadcrumbItems($maxDepth, $stopAtPageType, $showHidden);
829
        $template = new SSViewer('BreadcrumbsTemplate');
830
        return $template->process($this->customise(new ArrayData(array(
831
            "Pages" => $pages,
832
            "Unlinked" => $unlinked
833
        ))));
834
    }
835
836
837
    /**
838
     * Returns a list of breadcrumbs for the current page.
839
     *
840
     * @param int $maxDepth The maximum depth to traverse.
841
     * @param boolean|string $stopAtPageType ClassName of a page to stop the upwards traversal.
842
     * @param boolean $showHidden Include pages marked with the attribute ShowInMenus = 0
843
     *
844
     * @return ArrayList
845
    */
846
    public function getBreadcrumbItems($maxDepth = 20, $stopAtPageType = false, $showHidden = false)
847
    {
848
        $page = $this;
849
        $pages = array();
850
851
        while ($page
852
            && $page->exists()
853
            && (!$maxDepth || count($pages) < $maxDepth)
854
            && (!$stopAtPageType || $page->ClassName != $stopAtPageType)
855
        ) {
856
            if ($showHidden || $page->ShowInMenus || ($page->ID == $this->ID)) {
857
                $pages[] = $page;
858
            }
859
860
            $page = $page->Parent();
861
        }
862
863
        return new ArrayList(array_reverse($pages));
864
    }
865
866
867
    /**
868
     * Make this page a child of another page.
869
     *
870
     * If the parent page does not exist, resolve it to a valid ID before updating this page's reference.
871
     *
872
     * @param SiteTree|int $item Either the parent object, or the parent ID
873
     */
874
    public function setParent($item)
875
    {
876
        if (is_object($item)) {
877
            if (!$item->exists()) {
878
                $item->write();
879
            }
880
            $this->setField("ParentID", $item->ID);
881
        } else {
882
            $this->setField("ParentID", $item);
883
        }
884
    }
885
886
    /**
887
     * Get the parent of this page.
888
     *
889
     * @return SiteTree Parent of this page
890
     */
891
    public function getParent()
892
    {
893
        if ($parentID = $this->getField("ParentID")) {
894
            return DataObject::get_by_id(self::class, $parentID);
895
        }
896
        return null;
897
    }
898
899
    /**
900
     * Return a string of the form "parent - page" or "grandparent - parent - page" using page titles
901
     *
902
     * @param int $level The maximum amount of levels to traverse.
903
     * @param string $separator Seperating string
904
     * @return string The resulting string
905
     */
906
    public function NestedTitle($level = 2, $separator = " - ")
907
    {
908
        $item = $this;
909
        $parts = [];
910
        while ($item && $level > 0) {
911
            $parts[] = $item->Title;
912
            $item = $item->getParent();
913
            $level--;
914
        }
915
        return implode($separator, array_reverse($parts));
916
    }
917
918
    /**
919
     * This function should return true if the current user can execute this action. It can be overloaded to customise
920
     * the security model for an application.
921
     *
922
     * Slightly altered from parent behaviour in {@link DataObject->can()}:
923
     * - Checks for existence of a method named "can<$perm>()" on the object
924
     * - Calls decorators and only returns for FALSE "vetoes"
925
     * - Falls back to {@link Permission::check()}
926
     * - Does NOT check for many-many relations named "Can<$perm>"
927
     *
928
     * @uses DataObjectDecorator->can()
929
     *
930
     * @param string $perm The permission to be checked, such as 'View'
931
     * @param Member $member The member whose permissions need checking. Defaults to the currently logged in user.
932
     * @param array $context Context argument for canCreate()
933
     * @return bool True if the the member is allowed to do the given action
934
     */
935
    public function can($perm, $member = null, $context = array())
936
    {
937 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...
938
            $member = Member::currentUserID();
939
        }
940
941
        if ($member && Permission::checkMember($member, "ADMIN")) {
942
            return true;
943
        }
944
945
        if (is_string($perm) && method_exists($this, 'can' . ucfirst($perm))) {
946
            $method = 'can' . ucfirst($perm);
947
            return $this->$method($member);
948
        }
949
950
        $results = $this->extend('can', $member);
951
        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...
952
            if (!min($results)) {
953
                return false;
954
            }
955
        }
956
957
        return ($member && Permission::checkMember($member, $perm));
958
    }
959
960
    /**
961
     * This function should return true if the current user can add children to this page. It can be overloaded to
962
     * customise the security model for an application.
963
     *
964
     * Denies permission if any of the following conditions is true:
965
     * - alternateCanAddChildren() on a extension returns false
966
     * - canEdit() is not granted
967
     * - There are no classes defined in {@link $allowed_children}
968
     *
969
     * @uses SiteTreeExtension->canAddChildren()
970
     * @uses canEdit()
971
     * @uses $allowed_children
972
     *
973
     * @param Member|int $member
974
     * @return bool True if the current user can add children
975
     */
976
    public function canAddChildren($member = null)
977
    {
978
        // Disable adding children to archived pages
979
        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...
980
            return false;
981
        }
982
983 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...
984
            $member = Member::currentUserID();
985
        }
986
987
        // Standard mechanism for accepting permission changes from extensions
988
        $extended = $this->extendedCan('canAddChildren', $member);
989
        if ($extended !== null) {
990
            return $extended;
991
        }
992
993
        // Default permissions
994
        if ($member && Permission::checkMember($member, "ADMIN")) {
995
            return true;
996
        }
997
998
        return $this->canEdit($member) && $this->stat('allowed_children') !== 'none';
999
    }
1000
1001
    /**
1002
     * This function should return true if the current user can view this page. It can be overloaded to customise the
1003
     * security model for an application.
1004
     *
1005
     * Denies permission if any of the following conditions is true:
1006
     * - canView() on any extension returns false
1007
     * - "CanViewType" directive is set to "Inherit" and any parent page return false for canView()
1008
     * - "CanViewType" directive is set to "LoggedInUsers" and no user is logged in
1009
     * - "CanViewType" directive is set to "OnlyTheseUsers" and user is not in the given groups
1010
     *
1011
     * @uses DataExtension->canView()
1012
     * @uses ViewerGroups()
1013
     *
1014
     * @param Member|int $member
1015
     * @return bool True if the current user can view this page
1016
     */
1017
    public function canView($member = null)
1018
    {
1019 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...
1020
            $member = Member::currentUserID();
1021
        }
1022
1023
        // Standard mechanism for accepting permission changes from extensions
1024
        $extended = $this->extendedCan('canView', $member);
1025
        if ($extended !== null) {
1026
            return $extended;
1027
        }
1028
1029
        // admin override
1030
        if ($member && Permission::checkMember($member, array("ADMIN", "SITETREE_VIEW_ALL"))) {
1031
            return true;
1032
        }
1033
1034
        // Orphaned pages (in the current stage) are unavailable, except for admins via the CMS
1035
        if ($this->isOrphaned()) {
1036
            return false;
1037
        }
1038
1039
        // check for empty spec
1040
        if (!$this->CanViewType || $this->CanViewType == 'Anyone') {
1041
            return true;
1042
        }
1043
1044
        // check for inherit
1045
        if ($this->CanViewType == 'Inherit') {
1046
            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...
1047
                return $this->Parent()->canView($member);
1048
            } else {
1049
                return $this->getSiteConfig()->canViewPages($member);
1050
            }
1051
        }
1052
1053
        // check for any logged-in users
1054
        if ($this->CanViewType == 'LoggedInUsers' && $member) {
1055
            return true;
1056
        }
1057
1058
        // check for specific groups
1059
        if ($member && is_numeric($member)) {
1060
            $member = DataObject::get_by_id(Member::class, $member);
1061
        }
1062
        if ($this->CanViewType == 'OnlyTheseUsers'
1063
            && $member
1064
            && $member->inGroups($this->ViewerGroups())
1065
        ) {
1066
            return true;
1067
        }
1068
1069
        return false;
1070
    }
1071
1072
    /**
1073
     * Check if this page can be published
1074
     *
1075
     * @param Member $member
1076
     * @return bool
1077
     */
1078
    public function canPublish($member = null)
1079
    {
1080
        if (!$member) {
1081
            $member = Member::currentUser();
1082
        }
1083
1084
        // Check extension
1085
        $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...
1086
        if ($extended !== null) {
1087
            return $extended;
1088
        }
1089
1090
        if (Permission::checkMember($member, "ADMIN")) {
1091
            return true;
1092
        }
1093
1094
        // Default to relying on edit permission
1095
        return $this->canEdit($member);
1096
    }
1097
1098
    /**
1099
     * This function should return true if the current user can delete this page. It can be overloaded to customise the
1100
     * security model for an application.
1101
     *
1102
     * Denies permission if any of the following conditions is true:
1103
     * - canDelete() returns false on any extension
1104
     * - canEdit() returns false
1105
     * - any descendant page returns false for canDelete()
1106
     *
1107
     * @uses canDelete()
1108
     * @uses SiteTreeExtension->canDelete()
1109
     * @uses canEdit()
1110
     *
1111
     * @param Member $member
1112
     * @return bool True if the current user can delete this page
1113
     */
1114
    public function canDelete($member = null)
1115
    {
1116 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...
1117
            $memberID = $member->ID;
1118
        } elseif (is_numeric($member)) {
1119
            $memberID = $member;
1120
        } else {
1121
            $memberID = Member::currentUserID();
1122
        }
1123
1124
        // Standard mechanism for accepting permission changes from extensions
1125
        $extended = $this->extendedCan('canDelete', $memberID);
1126
        if ($extended !== null) {
1127
            return $extended;
1128
        }
1129
1130
        // Default permission check
1131
        if ($memberID && Permission::checkMember($memberID, array("ADMIN", "SITETREE_EDIT_ALL"))) {
1132
            return true;
1133
        }
1134
1135
        // Regular canEdit logic is handled by can_edit_multiple
1136
        $results = self::can_delete_multiple(array($this->ID), $memberID);
1137
1138
        // If this page no longer exists in stage/live results won't contain the page.
1139
        // Fail-over to false
1140
        return isset($results[$this->ID]) ? $results[$this->ID] : false;
1141
    }
1142
1143
    /**
1144
     * This function should return true if the current user can create new pages of this class, regardless of class. It
1145
     * can be overloaded to customise the security model for an application.
1146
     *
1147
     * By default, permission to create at the root level is based on the SiteConfig configuration, and permission to
1148
     * create beneath a parent is based on the ability to edit that parent page.
1149
     *
1150
     * Use {@link canAddChildren()} to control behaviour of creating children under this page.
1151
     *
1152
     * @uses $can_create
1153
     * @uses DataExtension->canCreate()
1154
     *
1155
     * @param Member $member
1156
     * @param array $context Optional array which may contain array('Parent' => $parentObj)
1157
     *                       If a parent page is known, it will be checked for validity.
1158
     *                       If omitted, it will be assumed this is to be created as a top level page.
1159
     * @return bool True if the current user can create pages on this class.
1160
     */
1161
    public function canCreate($member = null, $context = array())
1162
    {
1163 View Code Duplication
        if (!$member || !(is_a($member, Member::class)) || 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...
1164
            $member = Member::currentUserID();
1165
        }
1166
1167
        // Check parent (custom canCreate option for SiteTree)
1168
        // Block children not allowed for this parent type
1169
        $parent = isset($context['Parent']) ? $context['Parent'] : null;
1170
        if ($parent && !in_array(static::class, $parent->allowedChildren())) {
1171
            return false;
1172
        }
1173
1174
        // Standard mechanism for accepting permission changes from extensions
1175
        $extended = $this->extendedCan(__FUNCTION__, $member, $context);
1176
        if ($extended !== null) {
1177
            return $extended;
1178
        }
1179
1180
        // Check permission
1181
        if ($member && Permission::checkMember($member, "ADMIN")) {
1182
            return true;
1183
        }
1184
1185
        // Fall over to inherited permissions
1186
        if ($parent && $parent->exists()) {
1187
            return $parent->canAddChildren($member);
1188
        } else {
1189
            // This doesn't necessarily mean we are creating a root page, but that
1190
            // we don't know if there is a parent, so default to this permission
1191
            return SiteConfig::current_site_config()->canCreateTopLevel($member);
1192
        }
1193
    }
1194
1195
    /**
1196
     * This function should return true if the current user can edit this page. It can be overloaded to customise the
1197
     * security model for an application.
1198
     *
1199
     * Denies permission if any of the following conditions is true:
1200
     * - canEdit() on any extension returns false
1201
     * - canView() return false
1202
     * - "CanEditType" directive is set to "Inherit" and any parent page return false for canEdit()
1203
     * - "CanEditType" directive is set to "LoggedInUsers" and no user is logged in or doesn't have the
1204
     *   CMS_Access_CMSMAIN permission code
1205
     * - "CanEditType" directive is set to "OnlyTheseUsers" and user is not in the given groups
1206
     *
1207
     * @uses canView()
1208
     * @uses EditorGroups()
1209
     * @uses DataExtension->canEdit()
1210
     *
1211
     * @param Member $member Set to false if you want to explicitly test permissions without a valid user (useful for
1212
     *                       unit tests)
1213
     * @return bool True if the current user can edit this page
1214
     */
1215
    public function canEdit($member = null)
1216
    {
1217 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...
1218
            $memberID = $member->ID;
1219
        } elseif (is_numeric($member)) {
1220
            $memberID = $member;
1221
        } else {
1222
            $memberID = Member::currentUserID();
1223
        }
1224
1225
        // Standard mechanism for accepting permission changes from extensions
1226
        $extended = $this->extendedCan('canEdit', $memberID);
1227
        if ($extended !== null) {
1228
            return $extended;
1229
        }
1230
1231
        // Default permissions
1232
        if ($memberID && Permission::checkMember($memberID, array("ADMIN", "SITETREE_EDIT_ALL"))) {
1233
            return true;
1234
        }
1235
1236
        if ($this->ID) {
1237
            // Regular canEdit logic is handled by can_edit_multiple
1238
            $results = self::can_edit_multiple(array($this->ID), $memberID);
1239
1240
            // If this page no longer exists in stage/live results won't contain the page.
1241
            // Fail-over to false
1242
            return isset($results[$this->ID]) ? $results[$this->ID] : false;
1243
1244
        // Default for unsaved pages
1245
        } else {
1246
            return $this->getSiteConfig()->canEditPages($member);
1247
        }
1248
    }
1249
1250
    /**
1251
     * Stub method to get the site config, unless the current class can provide an alternate.
1252
     *
1253
     * @return SiteConfig
1254
     */
1255
    public function getSiteConfig()
1256
    {
1257
        $configs = $this->invokeWithExtensions('alternateSiteConfig');
1258
        foreach (array_filter($configs) as $config) {
1259
            return $config;
1260
        }
1261
1262
        return SiteConfig::current_site_config();
1263
    }
1264
1265
    /**
1266
     * Pre-populate the cache of canEdit, canView, canDelete, canPublish permissions. This method will use the static
1267
     * can_(perm)_multiple method for efficiency.
1268
     *
1269
     * @param string          $permission    The permission: edit, view, publish, approve, etc.
1270
     * @param array           $ids           An array of page IDs
1271
     * @param callable|string $batchCallback The function/static method to call to calculate permissions.  Defaults
1272
     *                                       to 'SiteTree::can_(permission)_multiple'
1273
     */
1274
    public static function prepopulate_permission_cache($permission = 'CanEditType', $ids = [], $batchCallback = null)
1275
    {
1276
        if (!$batchCallback) {
1277
            $batchCallback = self::class . "::can_{$permission}_multiple";
1278
        }
1279
1280
        if (is_callable($batchCallback)) {
1281
            call_user_func($batchCallback, $ids, Member::currentUserID(), false);
1282
        } else {
1283
            user_error("SiteTree::prepopulate_permission_cache can't calculate '$permission' "
1284
                . "with callback '$batchCallback'", E_USER_WARNING);
1285
        }
1286
    }
1287
1288
    /**
1289
     * This method is NOT a full replacement for the individual can*() methods, e.g. {@link canEdit()}. Rather than
1290
     * checking (potentially slow) PHP logic, it relies on the database group associations, e.g. the "CanEditType" field
1291
     * plus the "SiteTree_EditorGroups" many-many table. By batch checking multiple records, we can combine the queries
1292
     * efficiently.
1293
     *
1294
     * Caches based on $typeField data. To invalidate the cache, use {@link SiteTree::reset()} or set the $useCached
1295
     * property to FALSE.
1296
     *
1297
     * @param array  $ids              Of {@link SiteTree} IDs
1298
     * @param int    $memberID         Member ID
1299
     * @param string $typeField        A property on the data record, e.g. "CanEditType".
1300
     * @param string $groupJoinTable   A many-many table name on this record, e.g. "SiteTree_EditorGroups"
1301
     * @param string $siteConfigMethod Method to call on {@link SiteConfig} for toplevel items, e.g. "canEdit"
1302
     * @param string $globalPermission If the member doesn't have this permission code, don't bother iterating deeper
1303
     * @param bool   $useCached
1304
     * @return array An map of {@link SiteTree} ID keys to boolean values
1305
     */
1306
    public static function batch_permission_check(
1307
        $ids,
1308
        $memberID,
1309
        $typeField,
1310
        $groupJoinTable,
1311
        $siteConfigMethod,
1312
        $globalPermission = null,
1313
        $useCached = true
1314
    ) {
1315
        if ($globalPermission === null) {
1316
            $globalPermission = array('CMS_ACCESS_LeftAndMain', 'CMS_ACCESS_CMSMain');
1317
        }
1318
1319
        // Sanitise the IDs
1320
        $ids = array_filter($ids, 'is_numeric');
1321
1322
        // This is the name used on the permission cache
1323
        // converts something like 'CanEditType' to 'edit'.
1324
        $cacheKey = strtolower(substr($typeField, 3, -4)) . "-$memberID";
1325
1326
        // Default result: nothing editable
1327
        $result = array_fill_keys($ids, false);
1328
        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...
1329
            // Look in the cache for values
1330
            if ($useCached && isset(self::$cache_permissions[$cacheKey])) {
1331
                $cachedValues = array_intersect_key(self::$cache_permissions[$cacheKey], $result);
1332
1333
                // If we can't find everything in the cache, then look up the remainder separately
1334
                $uncachedValues = array_diff_key($result, self::$cache_permissions[$cacheKey]);
1335
                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...
1336
                    $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 1316 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...
1337
                }
1338
                return $cachedValues;
1339
            }
1340
1341
            // If a member doesn't have a certain permission then they can't edit anything
1342
            if (!$memberID || ($globalPermission && !Permission::checkMember($memberID, $globalPermission))) {
1343
                return $result;
1344
            }
1345
1346
            // Placeholder for parameterised ID list
1347
            $idPlaceholders = DB::placeholders($ids);
1348
1349
            // If page can't be viewed, don't grant edit permissions to do - implement can_view_multiple(), so this can
1350
            // be enabled
1351
            //$ids = array_keys(array_filter(self::can_view_multiple($ids, $memberID)));
1352
1353
            // Get the groups that the given member belongs to
1354
            /** @var Member $member */
1355
            $member = DataObject::get_by_id(Member::class, $memberID);
1356
            $groupIDs = $member->Groups()->column("ID");
1357
            $SQL_groupList = implode(", ", $groupIDs);
1358
            if (!$SQL_groupList) {
1359
                $SQL_groupList = '0';
1360
            }
1361
1362
            $combinedStageResult = array();
1363
1364
            foreach (array(Versioned::DRAFT, Versioned::LIVE) as $stage) {
1365
                // Start by filling the array with the pages that actually exist
1366
                /** @skipUpgrade */
1367
                $table = ($stage=='Stage') ? "SiteTree" : "SiteTree_$stage";
1368
1369
                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...
1370
                    $idQuery = "SELECT \"ID\" FROM \"$table\" WHERE \"ID\" IN ($idPlaceholders)";
1371
                    $stageIds = DB::prepared_query($idQuery, $ids)->column();
1372
                } else {
1373
                    $stageIds = array();
1374
                }
1375
                $result = array_fill_keys($stageIds, false);
1376
1377
                // Get the uninherited permissions
1378
                $uninheritedPermissions = Versioned::get_by_stage("SilverStripe\\CMS\\Model\\SiteTree", $stage)
1379
                    ->where(array(
1380
                        "(\"$typeField\" = 'LoggedInUsers' OR
1381
						(\"$typeField\" = 'OnlyTheseUsers' AND \"$groupJoinTable\".\"SiteTreeID\" IS NOT NULL))
1382
						AND \"SiteTree\".\"ID\" IN ($idPlaceholders)"
1383
                        => $ids
1384
                    ))
1385
                    ->leftJoin($groupJoinTable, "\"$groupJoinTable\".\"SiteTreeID\" = \"SiteTree\".\"ID\" AND \"$groupJoinTable\".\"GroupID\" IN ($SQL_groupList)");
1386
1387
                if ($uninheritedPermissions) {
1388
                    // Set all the relevant items in $result to true
1389
                    $result = array_fill_keys($uninheritedPermissions->column('ID'), true) + $result;
1390
                }
1391
1392
                // Get permissions that are inherited
1393
                $potentiallyInherited = Versioned::get_by_stage(
1394
                    "SilverStripe\\CMS\\Model\\SiteTree",
1395
                    $stage,
1396
                    array("\"$typeField\" = 'Inherit' AND \"SiteTree\".\"ID\" IN ($idPlaceholders)" => $ids)
1397
                );
1398
1399
                if ($potentiallyInherited) {
1400
                    // Group $potentiallyInherited by ParentID; we'll look at the permission of all those parents and
1401
                    // then see which ones the user has permission on
1402
                    $groupedByParent = array();
1403
                    foreach ($potentiallyInherited as $item) {
1404
                        /** @var SiteTree $item */
1405
                        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...
1406
                            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...
1407
                                $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...
1408
                            }
1409
                            $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...
1410
                        } else {
1411
                            // Might return different site config based on record context, e.g. when subsites module
1412
                            // is used
1413
                            $siteConfig = $item->getSiteConfig();
1414
                            $result[$item->ID] = $siteConfig->{$siteConfigMethod}($memberID);
1415
                        }
1416
                    }
1417
1418
                    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...
1419
                        $actuallyInherited = self::batch_permission_check(array_keys($groupedByParent), $memberID, $typeField, $groupJoinTable, $siteConfigMethod);
1420
                        if ($actuallyInherited) {
1421
                            $parentIDs = array_keys(array_filter($actuallyInherited));
1422
                            foreach ($parentIDs as $parentID) {
1423
                                // Set all the relevant items in $result to true
1424
                                $result = array_fill_keys($groupedByParent[$parentID], true) + $result;
1425
                            }
1426
                        }
1427
                    }
1428
                }
1429
1430
                $combinedStageResult = $combinedStageResult + $result;
1431
            }
1432
        }
1433
1434
        if (isset($combinedStageResult)) {
1435
            // Cache the results
1436
            if (empty(self::$cache_permissions[$cacheKey])) {
1437
                self::$cache_permissions[$cacheKey] = array();
1438
            }
1439
            self::$cache_permissions[$cacheKey] = $combinedStageResult + self::$cache_permissions[$cacheKey];
1440
            return $combinedStageResult;
1441
        } else {
1442
            return array();
1443
        }
1444
    }
1445
1446
    /**
1447
     * Get the 'can edit' information for a number of SiteTree pages.
1448
     *
1449
     * @param array $ids       An array of IDs of the SiteTree pages to look up
1450
     * @param int   $memberID  ID of member
1451
     * @param bool  $useCached Return values from the permission cache if they exist
1452
     * @return array A map where the IDs are keys and the values are booleans stating whether the given page can be
1453
     *                         edited
1454
     */
1455
    public static function can_edit_multiple($ids, $memberID, $useCached = true)
1456
    {
1457
        return self::batch_permission_check($ids, $memberID, 'CanEditType', 'SiteTree_EditorGroups', 'canEditPages', null, $useCached);
1458
    }
1459
1460
    /**
1461
     * Get the 'can edit' information for a number of SiteTree pages.
1462
     *
1463
     * @param array $ids       An array of IDs of the SiteTree pages to look up
1464
     * @param int   $memberID  ID of member
1465
     * @param bool  $useCached Return values from the permission cache if they exist
1466
     * @return array
1467
     */
1468
    public static function can_delete_multiple($ids, $memberID, $useCached = true)
1469
    {
1470
        $deletable = array();
1471
        $result = array_fill_keys($ids, false);
1472
        $cacheKey = "delete-$memberID";
1473
1474
        // Look in the cache for values
1475
        if ($useCached && isset(self::$cache_permissions[$cacheKey])) {
1476
            $cachedValues = array_intersect_key(self::$cache_permissions[$cacheKey], $result);
1477
1478
            // If we can't find everything in the cache, then look up the remainder separately
1479
            $uncachedValues = array_diff_key($result, self::$cache_permissions[$cacheKey]);
1480
            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...
1481
                $cachedValues = self::can_delete_multiple(array_keys($uncachedValues), $memberID, false)
1482
                    + $cachedValues;
1483
            }
1484
            return $cachedValues;
1485
        }
1486
1487
        // You can only delete pages that you can edit
1488
        $editableIDs = array_keys(array_filter(self::can_edit_multiple($ids, $memberID)));
1489
        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...
1490
            // You can only delete pages whose children you can delete
1491
            $editablePlaceholders = DB::placeholders($editableIDs);
1492
            $childRecords = SiteTree::get()->where(array(
1493
                "\"SiteTree\".\"ParentID\" IN ($editablePlaceholders)" => $editableIDs
1494
            ));
1495
            if ($childRecords) {
1496
                $children = $childRecords->map("ID", "ParentID");
1497
1498
                // Find out the children that can be deleted
1499
                $deletableChildren = self::can_delete_multiple($children->keys(), $memberID);
1500
1501
                // Get a list of all the parents that have no undeletable children
1502
                $deletableParents = array_fill_keys($editableIDs, true);
1503
                foreach ($deletableChildren as $id => $canDelete) {
1504
                    if (!$canDelete) {
1505
                        unset($deletableParents[$children[$id]]);
1506
                    }
1507
                }
1508
1509
                // Use that to filter the list of deletable parents that have children
1510
                $deletableParents = array_keys($deletableParents);
1511
1512
                // Also get the $ids that don't have children
1513
                $parents = array_unique($children->values());
1514
                $deletableLeafNodes = array_diff($editableIDs, $parents);
1515
1516
                // Combine the two
1517
                $deletable = array_merge($deletableParents, $deletableLeafNodes);
1518
            } else {
1519
                $deletable = $editableIDs;
1520
            }
1521
        }
1522
1523
        // Convert the array of deletable IDs into a map of the original IDs with true/false as the value
1524
        return array_fill_keys($deletable, true) + array_fill_keys($ids, false);
1525
    }
1526
1527
    /**
1528
     * Collate selected descendants of this page.
1529
     *
1530
     * {@link $condition} will be evaluated on each descendant, and if it is succeeds, that item will be added to the
1531
     * $collator array.
1532
     *
1533
     * @param string $condition The PHP condition to be evaluated. The page will be called $item
1534
     * @param array  $collator  An array, passed by reference, to collect all of the matching descendants.
1535
     * @return bool
1536
     */
1537
    public function collateDescendants($condition, &$collator)
1538
    {
1539
        $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...
1540
        if ($children) {
1541
            foreach ($children as $item) {
1542
                if (eval("return $condition;")) {
1543
                    $collator[] = $item;
1544
                }
1545
                /** @var SiteTree $item */
1546
                $item->collateDescendants($condition, $collator);
1547
            }
1548
            return true;
1549
        }
1550
        return false;
1551
    }
1552
1553
    /**
1554
     * Return the title, description, keywords and language metatags.
1555
     *
1556
     * @todo Move <title> tag in separate getter for easier customization and more obvious usage
1557
     *
1558
     * @param bool $includeTitle Show default <title>-tag, set to false for custom templating
1559
     * @return string The XHTML metatags
1560
     */
1561
    public function MetaTags($includeTitle = true)
1562
    {
1563
        $tags = array();
1564
        if ($includeTitle && strtolower($includeTitle) != 'false') {
1565
            $tags[] = FormField::create_tag('title', array(), $this->obj('Title')->forTemplate());
1566
        }
1567
1568
        $generator = trim(Config::inst()->get(self::class, 'meta_generator'));
1569
        if (!empty($generator)) {
1570
            $tags[] = FormField::create_tag('meta', array(
1571
                'name' => 'generator',
1572
                'content' => $generator,
1573
            ));
1574
        }
1575
1576
        $charset = ContentNegotiator::config()->uninherited('encoding');
1577
        $tags[] = FormField::create_tag('meta', array(
1578
            'http-equiv' => 'Content-Type',
1579
            'content' => 'text/html; charset=' . $charset,
1580
        ));
1581
        if ($this->MetaDescription) {
1582
            $tags[] = FormField::create_tag('meta', array(
1583
                'name' => 'description',
1584
                'content' => $this->MetaDescription,
1585
            ));
1586
        }
1587
1588
        if (Permission::check('CMS_ACCESS_CMSMain')
1589
            && !$this instanceof ErrorPage
1590
            && $this->ID > 0
1591
        ) {
1592
            $tags[] = FormField::create_tag('meta', array(
1593
                'name' => 'x-page-id',
1594
                'content' => $this->obj('ID')->forTemplate(),
1595
            ));
1596
            $tags[] = FormField::create_tag('meta', array(
1597
                'name' => 'x-cms-edit-link',
1598
                'content' => $this->obj('CMSEditLink')->forTemplate(),
1599
            ));
1600
        }
1601
1602
        $tags = implode("\n", $tags);
1603
        if ($this->ExtraMeta) {
1604
            $tags .= $this->obj('ExtraMeta')->forTemplate();
1605
        }
1606
1607
        $this->extend('MetaTags', $tags);
1608
1609
        return $tags;
1610
    }
1611
1612
    /**
1613
     * Returns the object that contains the content that a user would associate with this page.
1614
     *
1615
     * Ordinarily, this is just the page itself, but for example on RedirectorPages or VirtualPages ContentSource() will
1616
     * return the page that is linked to.
1617
     *
1618
     * @return $this
1619
     */
1620
    public function ContentSource()
1621
    {
1622
        return $this;
1623
    }
1624
1625
    /**
1626
     * Add default records to database.
1627
     *
1628
     * This function is called whenever the database is built, after the database tables have all been created. Overload
1629
     * this to add default records when the database is built, but make sure you call parent::requireDefaultRecords().
1630
     */
1631
    public function requireDefaultRecords()
1632
    {
1633
        parent::requireDefaultRecords();
1634
1635
        // default pages
1636
        if (static::class == self::class && $this->config()->create_default_pages) {
1637
            if (!SiteTree::get_by_link(RootURLController::config()->default_homepage_link)) {
1638
                $homepage = new Page();
1639
                $homepage->Title = _t('SiteTree.DEFAULTHOMETITLE', 'Home');
1640
                $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>');
1641
                $homepage->URLSegment = RootURLController::config()->default_homepage_link;
1642
                $homepage->Sort = 1;
1643
                $homepage->write();
1644
                $homepage->copyVersionToStage(Versioned::DRAFT, Versioned::LIVE);
1645
                $homepage->flushCache();
1646
                DB::alteration_message('Home page created', 'created');
1647
            }
1648
1649
            if (DB::query("SELECT COUNT(*) FROM \"SiteTree\"")->value() == 1) {
1650
                $aboutus = new Page();
1651
                $aboutus->Title = _t('SiteTree.DEFAULTABOUTTITLE', 'About Us');
1652
                $aboutus->Content = _t(
1653
                    'SiteTree.DEFAULTABOUTCONTENT',
1654
                    '<p>You can fill this page out with your own content, or delete it and create your own pages.</p>'
1655
                );
1656
                $aboutus->Sort = 2;
1657
                $aboutus->write();
1658
                $aboutus->copyVersionToStage(Versioned::DRAFT, Versioned::LIVE);
1659
                $aboutus->flushCache();
1660
                DB::alteration_message('About Us page created', 'created');
1661
1662
                $contactus = new Page();
1663
                $contactus->Title = _t('SiteTree.DEFAULTCONTACTTITLE', 'Contact Us');
1664
                $contactus->Content = _t(
1665
                    'SiteTree.DEFAULTCONTACTCONTENT',
1666
                    '<p>You can fill this page out with your own content, or delete it and create your own pages.</p>'
1667
                );
1668
                $contactus->Sort = 3;
1669
                $contactus->write();
1670
                $contactus->copyVersionToStage(Versioned::DRAFT, Versioned::LIVE);
1671
                $contactus->flushCache();
1672
                DB::alteration_message('Contact Us page created', 'created');
1673
            }
1674
        }
1675
    }
1676
1677
    protected function onBeforeWrite()
1678
    {
1679
        parent::onBeforeWrite();
1680
1681
        // If Sort hasn't been set, make this page come after it's siblings
1682
        if (!$this->Sort) {
1683
            $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...
1684
            $this->Sort = DB::prepared_query(
1685
                "SELECT MAX(\"Sort\") + 1 FROM \"SiteTree\" WHERE \"ParentID\" = ?",
1686
                array($parentID)
1687
            )->value();
1688
        }
1689
1690
        // If there is no URLSegment set, generate one from Title
1691
        $defaultSegment = $this->generateURLSegment(_t(
1692
            'CMSMain.NEWPAGE',
1693
            'New {pagetype}',
1694
            array('pagetype' => $this->i18n_singular_name())
1695
        ));
1696
        if ((!$this->URLSegment || $this->URLSegment == $defaultSegment) && $this->Title) {
1697
            $this->URLSegment = $this->generateURLSegment($this->Title);
1698
        } elseif ($this->isChanged('URLSegment', 2)) {
1699
            // Do a strict check on change level, to avoid double encoding caused by
1700
            // bogus changes through forceChange()
1701
            $filter = URLSegmentFilter::create();
1702
            $this->URLSegment = $filter->filter($this->URLSegment);
1703
            // If after sanitising there is no URLSegment, give it a reasonable default
1704
            if (!$this->URLSegment) {
1705
                $this->URLSegment = "page-$this->ID";
1706
            }
1707
        }
1708
1709
        // Ensure that this object has a non-conflicting URLSegment value.
1710
        $count = 2;
1711
        while (!$this->validURLSegment()) {
1712
            $this->URLSegment = preg_replace('/-[0-9]+$/', null, $this->URLSegment) . '-' . $count;
1713
            $count++;
1714
        }
1715
1716
        $this->syncLinkTracking();
1717
1718
        // Check to see if we've only altered fields that shouldn't affect versioning
1719
        $fieldsIgnoredByVersioning = array('HasBrokenLink', 'Status', 'HasBrokenFile', 'ToDo', 'VersionID', 'SaveCount');
1720
        $changedFields = array_keys($this->getChangedFields(true, 2));
1721
1722
        // This more rigorous check is inline with the test that write() does to decide whether or not to write to the
1723
        // DB. We use that to avoid cluttering the system with a migrateVersion() call that doesn't get used
1724
        $oneChangedFields = array_keys($this->getChangedFields(true, 1));
1725
1726
        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...
1727
            // This will have the affect of preserving the versioning
1728
            $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...
1729
        }
1730
    }
1731
1732
    /**
1733
     * Trigger synchronisation of link tracking
1734
     *
1735
     * {@see SiteTreeLinkTracking::augmentSyncLinkTracking}
1736
     */
1737
    public function syncLinkTracking()
1738
    {
1739
        $this->extend('augmentSyncLinkTracking');
1740
    }
1741
1742
    public function onBeforeDelete()
1743
    {
1744
        parent::onBeforeDelete();
1745
1746
        // If deleting this page, delete all its children.
1747
        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...
1748
            foreach ($children as $child) {
1749
                /** @var SiteTree $child */
1750
                $child->delete();
1751
            }
1752
        }
1753
    }
1754
1755
    public function onAfterDelete()
1756
    {
1757
        // Need to flush cache to avoid outdated versionnumber references
1758
        $this->flushCache();
1759
1760
        // Need to mark pages depending to this one as broken
1761
        $dependentPages = $this->DependentPages();
1762
        if ($dependentPages) {
1763
            foreach ($dependentPages as $page) {
1764
            // $page->write() calls syncLinkTracking, which does all the hard work for us.
1765
                $page->write();
1766
            }
1767
        }
1768
1769
        parent::onAfterDelete();
1770
    }
1771
1772
    public function flushCache($persistent = true)
1773
    {
1774
        parent::flushCache($persistent);
1775
        $this->_cache_statusFlags = null;
1776
    }
1777
1778
    public function validate()
1779
    {
1780
        $result = parent::validate();
1781
1782
        // Allowed children validation
1783
        $parent = $this->getParent();
1784
        if ($parent && $parent->exists()) {
1785
            // No need to check for subclasses or instanceof, as allowedChildren() already
1786
            // deconstructs any inheritance trees already.
1787
            $allowed = $parent->allowedChildren();
1788
            $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...
1789
                ? $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...
1790
                : $this;
1791
            if (!in_array($subject->ClassName, $allowed)) {
1792
                $result->addError(
1793
                    _t(
1794
                        'SiteTree.PageTypeNotAllowed',
1795
                        'Page type "{type}" not allowed as child of this parent page',
1796
                        array('type' => $subject->i18n_singular_name())
1797
                    ),
1798
                    ValidationResult::TYPE_ERROR,
1799
                    'ALLOWED_CHILDREN'
1800
                );
1801
            }
1802
        }
1803
1804
        // "Can be root" validation
1805
        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...
1806
            $result->addError(
1807
                _t(
1808
                    'SiteTree.PageTypNotAllowedOnRoot',
1809
                    'Page type "{type}" is not allowed on the root level',
1810
                    array('type' => $this->i18n_singular_name())
1811
                ),
1812
                ValidationResult::TYPE_ERROR,
1813
                'CAN_BE_ROOT'
1814
            );
1815
        }
1816
1817
        return $result;
1818
    }
1819
1820
    /**
1821
     * Returns true if this object has a URLSegment value that does not conflict with any other objects. This method
1822
     * checks for:
1823
     *  - A page with the same URLSegment that has a conflict
1824
     *  - Conflicts with actions on the parent page
1825
     *  - A conflict caused by a root page having the same URLSegment as a class name
1826
     *
1827
     * @return bool
1828
     */
1829
    public function validURLSegment()
1830
    {
1831
        if (self::config()->nested_urls && $parent = $this->Parent()) {
1832
            if ($controller = ModelAsController::controller_for($parent)) {
1833
                if ($controller instanceof Controller && $controller->hasAction($this->URLSegment)) {
1834
                    return false;
1835
                }
1836
            }
1837
        }
1838
1839
        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...
1840
            if (class_exists($this->URLSegment) && is_subclass_of($this->URLSegment, RequestHandler::class)) {
0 ignored issues
show
Bug introduced by
Due to PHP Bug #53727, is_subclass_of might return inconsistent results on some PHP versions if \SilverStripe\Control\RequestHandler::class can be an interface. If so, you could instead use ReflectionClass::implementsInterface.
Loading history...
1841
                return false;
1842
            }
1843
        }
1844
1845
        // Filters by url, id, and parent
1846
        $filter = array('"SiteTree"."URLSegment"' => $this->URLSegment);
1847
        if ($this->ID) {
1848
            $filter['"SiteTree"."ID" <> ?'] = $this->ID;
1849
        }
1850
        if (self::config()->nested_urls) {
1851
            $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...
1852
        }
1853
1854
        // If any of the extensions return `0` consider the segment invalid
1855
        $extensionResponses = array_filter(
1856
            (array)$this->extend('augmentValidURLSegment'),
1857
            function ($response) {
1858
                return !is_null($response);
1859
            }
1860
        );
1861
        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...
1862
            return min($extensionResponses);
1863
        }
1864
1865
        // Check existence
1866
        return !DataObject::get(self::class, $filter)->exists();
1867
    }
1868
1869
    /**
1870
     * Generate a URL segment based on the title provided.
1871
     *
1872
     * If {@link Extension}s wish to alter URL segment generation, they can do so by defining
1873
     * updateURLSegment(&$url, $title).  $url will be passed by reference and should be modified. $title will contain
1874
     * the title that was originally used as the source of this generated URL. This lets extensions either start from
1875
     * scratch, or incrementally modify the generated URL.
1876
     *
1877
     * @param string $title Page title
1878
     * @return string Generated url segment
1879
     */
1880
    public function generateURLSegment($title)
1881
    {
1882
        $filter = URLSegmentFilter::create();
1883
        $t = $filter->filter($title);
1884
1885
        // Fallback to generic page name if path is empty (= no valid, convertable characters)
1886
        if (!$t || $t == '-' || $t == '-1') {
1887
            $t = "page-$this->ID";
1888
        }
1889
1890
        // Hook for extensions
1891
        $this->extend('updateURLSegment', $t, $title);
1892
1893
        return $t;
1894
    }
1895
1896
    /**
1897
     * Gets the URL segment for the latest draft version of this page.
1898
     *
1899
     * @return string
1900
     */
1901
    public function getStageURLSegment()
1902
    {
1903
        $stageRecord = Versioned::get_one_by_stage(self::class, Versioned::DRAFT, array(
1904
            '"SiteTree"."ID"' => $this->ID
1905
        ));
1906
        return ($stageRecord) ? $stageRecord->URLSegment : null;
1907
    }
1908
1909
    /**
1910
     * Gets the URL segment for the currently published version of this page.
1911
     *
1912
     * @return string
1913
     */
1914
    public function getLiveURLSegment()
1915
    {
1916
        $liveRecord = Versioned::get_one_by_stage(self::class, Versioned::LIVE, array(
1917
            '"SiteTree"."ID"' => $this->ID
1918
        ));
1919
        return ($liveRecord) ? $liveRecord->URLSegment : null;
1920
    }
1921
1922
    /**
1923
     * Returns the pages that depend on this page. This includes virtual pages, pages that link to it, etc.
1924
     *
1925
     * @param bool $includeVirtuals Set to false to exlcude virtual pages.
1926
     * @return ArrayList
1927
     */
1928
    public function DependentPages($includeVirtuals = true)
1929
    {
1930
        if (class_exists('Subsite')) {
1931
            $origDisableSubsiteFilter = Subsite::$disable_subsite_filter;
1932
            Subsite::disable_subsite_filter(true);
1933
        }
1934
1935
        // Content links
1936
        $items = new ArrayList();
1937
1938
        // We merge all into a regular SS_List, because DataList doesn't support merge
1939
        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...
1940
            $linkList = new ArrayList();
1941
            foreach ($contentLinks as $item) {
1942
                $item->DependentLinkType = 'Content link';
1943
                $linkList->push($item);
1944
            }
1945
            $items->merge($linkList);
1946
        }
1947
1948
        // Virtual pages
1949
        if ($includeVirtuals) {
1950
            $virtuals = $this->VirtualPages();
1951
            if ($virtuals) {
1952
                $virtualList = new ArrayList();
1953
                foreach ($virtuals as $item) {
1954
                    $item->DependentLinkType = 'Virtual page';
1955
                    $virtualList->push($item);
1956
                }
1957
                $items->merge($virtualList);
1958
            }
1959
        }
1960
1961
        // Redirector pages
1962
        $redirectors = RedirectorPage::get()->where(array(
1963
            '"RedirectorPage"."RedirectionType"' => 'Internal',
1964
            '"RedirectorPage"."LinkToID"' => $this->ID
1965
        ));
1966
        if ($redirectors) {
1967
            $redirectorList = new ArrayList();
1968
            foreach ($redirectors as $item) {
1969
                $item->DependentLinkType = 'Redirector page';
1970
                $redirectorList->push($item);
1971
            }
1972
            $items->merge($redirectorList);
1973
        }
1974
1975
        if (class_exists('Subsite')) {
1976
            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...
1977
        }
1978
1979
        return $items;
1980
    }
1981
1982
    /**
1983
     * Return all virtual pages that link to this page.
1984
     *
1985
     * @return DataList
1986
     */
1987
    public function VirtualPages()
1988
    {
1989
        $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...
1990
1991
        // Disable subsite filter for these pages
1992
        if ($pages instanceof DataList) {
1993
            return $pages->setDataQueryParam('Subsite.filter', false);
1994
        } else {
1995
            return $pages;
1996
        }
1997
    }
1998
1999
    /**
2000
     * Returns a FieldList with which to create the main editing form.
2001
     *
2002
     * You can override this in your child classes to add extra fields - first get the parent fields using
2003
     * parent::getCMSFields(), then use addFieldToTab() on the FieldList.
2004
     *
2005
     * See {@link getSettingsFields()} for a different set of fields concerned with configuration aspects on the record,
2006
     * e.g. access control.
2007
     *
2008
     * @return FieldList The fields to be displayed in the CMS
2009
     */
2010
    public function getCMSFields()
2011
    {
2012
        // Status / message
2013
        // Create a status message for multiple parents
2014
        if ($this->ID && is_numeric($this->ID)) {
2015
            $linkedPages = $this->VirtualPages();
2016
2017
            $parentPageLinks = array();
2018
2019
            if ($linkedPages->count() > 0) {
2020
                /** @var VirtualPage $linkedPage */
2021
                foreach ($linkedPages as $linkedPage) {
2022
                    $parentPage = $linkedPage->Parent();
2023
                    if ($parentPage && $parentPage->exists()) {
2024
                        $link = Convert::raw2att($parentPage->CMSEditLink());
2025
                        $title = Convert::raw2xml($parentPage->Title);
2026
                    } else {
2027
                        $link = CMSPageEditController::singleton()->Link('show');
2028
                        $title = _t('SiteTree.TOPLEVEL', 'Site Content (Top Level)');
2029
                    }
2030
                    $parentPageLinks[] = "<a class=\"cmsEditlink\" href=\"{$link}\">{$title}</a>";
2031
                }
2032
2033
                $lastParent = array_pop($parentPageLinks);
2034
                $parentList = "'$lastParent'";
2035
2036
                if (count($parentPageLinks)) {
2037
                    $parentList = "'" . implode("', '", $parentPageLinks) . "' and "
2038
                        . $parentList;
2039
                }
2040
2041
                $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...
2042
                    'SiteTree.APPEARSVIRTUALPAGES',
2043
                    "This content also appears on the virtual pages in the {title} sections.",
2044
                    array('title' => $parentList)
2045
                );
2046
            }
2047
        }
2048
2049
        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...
2050
            $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...
2051
        }
2052
2053
        $dependentNote = '';
2054
        $dependentTable = new LiteralField('DependentNote', '<p></p>');
2055
2056
        // Create a table for showing pages linked to this one
2057
        $dependentPages = $this->DependentPages();
2058
        $dependentPagesCount = $dependentPages->count();
2059
        if ($dependentPagesCount) {
2060
            $dependentColumns = array(
2061
                'Title' => $this->fieldLabel('Title'),
2062
                'AbsoluteLink' => _t('SiteTree.DependtPageColumnURL', 'URL'),
2063
                'DependentLinkType' => _t('SiteTree.DependtPageColumnLinkType', 'Link type'),
2064
            );
2065
            if (class_exists('Subsite')) {
2066
                $dependentColumns['Subsite.Title'] = singleton('Subsite')->i18n_singular_name();
2067
            }
2068
2069
            $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>');
2070
            $dependentTable = GridField::create(
2071
                'DependentPages',
2072
                false,
2073
                $dependentPages
2074
            );
2075
            /** @var GridFieldDataColumns $dataColumns */
2076
            $dataColumns = $dependentTable->getConfig()->getComponentByType('SilverStripe\\Forms\\GridField\\GridFieldDataColumns');
2077
            $dataColumns
2078
                ->setDisplayFields($dependentColumns)
2079
                ->setFieldFormatting(array(
2080
                    'Title' => function ($value, &$item) {
2081
                        return sprintf(
2082
                            '<a href="admin/pages/edit/show/%d">%s</a>',
2083
                            (int)$item->ID,
2084
                            Convert::raw2xml($item->Title)
2085
                        );
2086
                    },
2087
                    '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...
2088
                        return sprintf(
2089
                            '<a href="%s" target="_blank">%s</a>',
2090
                            Convert::raw2xml($value),
2091
                            Convert::raw2xml($value)
2092
                        );
2093
                    }
2094
                ));
2095
        }
2096
2097
        $baseLink = Controller::join_links(
2098
            Director::absoluteBaseURL(),
2099
            (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...
2100
        );
2101
2102
        $urlsegment = SiteTreeURLSegmentField::create("URLSegment", $this->fieldLabel('URLSegment'))
2103
            ->setURLPrefix($baseLink)
2104
            ->setDefaultURL($this->generateURLSegment(_t(
2105
                'CMSMain.NEWPAGE',
2106
                'New {pagetype}',
2107
                array('pagetype' => $this->i18n_singular_name())
2108
            )));
2109
        $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...
2110
            ? $this->fieldLabel('LinkChangeNote')
2111
            : '';
2112
        if (!Config::inst()->get('SilverStripe\\View\\Parsers\\URLSegmentFilter', 'default_allow_multibyte')) {
2113
            $helpText .= _t('SiteTreeURLSegmentField.HelpChars', ' Special characters are automatically converted or removed.');
2114
        }
2115
        $urlsegment->setHelpText($helpText);
2116
2117
        $fields = new FieldList(
2118
            $rootTab = new TabSet(
2119
                "Root",
2120
                $tabMain = new Tab(
2121
                    'Main',
2122
                    new TextField("Title", $this->fieldLabel('Title')),
2123
                    $urlsegment,
2124
                    new TextField("MenuTitle", $this->fieldLabel('MenuTitle')),
2125
                    $htmlField = new HTMLEditorField("Content", _t('SiteTree.HTMLEDITORTITLE', "Content", 'HTML editor title')),
2126
                    ToggleCompositeField::create(
2127
                        'Metadata',
2128
                        _t('SiteTree.MetadataToggle', 'Metadata'),
2129
                        array(
2130
                            $metaFieldDesc = new TextareaField("MetaDescription", $this->fieldLabel('MetaDescription')),
2131
                            $metaFieldExtra = new TextareaField("ExtraMeta", $this->fieldLabel('ExtraMeta'))
2132
                        )
2133
                    )->setHeadingLevel(4)
2134
                ),
2135
                $tabDependent = new Tab(
2136
                    'Dependent',
2137
                    $dependentNote,
2138
                    $dependentTable
2139
                )
2140
            )
2141
        );
2142
        $htmlField->addExtraClass('stacked');
2143
2144
        // Help text for MetaData on page content editor
2145
        $metaFieldDesc
2146
            ->setRightTitle(
2147
                _t(
2148
                    'SiteTree.METADESCHELP',
2149
                    "Search engines use this content for displaying search results (although it will not influence their ranking)."
2150
                )
2151
            )
2152
            ->addExtraClass('help');
2153
        $metaFieldExtra
2154
            ->setRightTitle(
2155
                _t(
2156
                    'SiteTree.METAEXTRAHELP',
2157
                    "HTML tags for additional meta information. For example &lt;meta name=\"customName\" content=\"your custom content here\" /&gt;"
2158
                )
2159
            )
2160
            ->addExtraClass('help');
2161
2162
        // Conditional dependent pages tab
2163
        if ($dependentPagesCount) {
2164
            $tabDependent->setTitle(_t('SiteTree.TABDEPENDENT', "Dependent pages") . " ($dependentPagesCount)");
2165
        } else {
2166
            $fields->removeFieldFromTab('Root', 'Dependent');
2167
        }
2168
2169
        $tabMain->setTitle(_t('SiteTree.TABCONTENT', "Main Content"));
2170
2171
        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...
2172
            $obsoleteWarning = _t(
2173
                'SiteTree.OBSOLETECLASS',
2174
                "This page is of obsolete type {type}. Saving will reset its type and you may lose data",
2175
                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...
2176
            );
2177
2178
            $fields->addFieldToTab(
2179
                "Root.Main",
2180
                new LiteralField("ObsoleteWarningHeader", "<p class=\"message warning\">$obsoleteWarning</p>"),
2181
                "Title"
2182
            );
2183
        }
2184
2185
        if (file_exists(BASE_PATH . '/install.php')) {
2186
            $fields->addFieldToTab("Root.Main", new LiteralField(
2187
                "InstallWarningHeader",
2188
                "<p class=\"message warning\">" . _t(
2189
                    "SiteTree.REMOVE_INSTALL_WARNING",
2190
                    "Warning: You should remove install.php from this SilverStripe install for security reasons."
2191
                )
2192
                . "</p>"
2193
            ), "Title");
2194
        }
2195
2196
        if (self::$runCMSFieldsExtensions) {
2197
            $this->extend('updateCMSFields', $fields);
2198
        }
2199
2200
        return $fields;
2201
    }
2202
2203
2204
    /**
2205
     * Returns fields related to configuration aspects on this record, e.g. access control. See {@link getCMSFields()}
2206
     * for content-related fields.
2207
     *
2208
     * @return FieldList
2209
     */
2210
    public function getSettingsFields()
2211
    {
2212
        $groupsMap = array();
2213
        foreach (Group::get() as $group) {
2214
            // Listboxfield values are escaped, use ASCII char instead of &raquo;
2215
            $groupsMap[$group->ID] = $group->getBreadcrumbs(' > ');
2216
        }
2217
        asort($groupsMap);
2218
2219
        $fields = new FieldList(
2220
            $rootTab = new TabSet(
2221
                "Root",
2222
                $tabBehaviour = new Tab(
2223
                    'Settings',
2224
                    new DropdownField(
2225
                        "ClassName",
2226
                        $this->fieldLabel('ClassName'),
2227
                        $this->getClassDropdown()
2228
                    ),
2229
                    $parentTypeSelector = new CompositeField(
2230
                        $parentType = new OptionsetField("ParentType", _t("SiteTree.PAGELOCATION", "Page location"), array(
2231
                            "root" => _t("SiteTree.PARENTTYPE_ROOT", "Top-level page"),
2232
                            "subpage" => _t("SiteTree.PARENTTYPE_SUBPAGE", "Sub-page underneath a parent page"),
2233
                        )),
2234
                        $parentIDField = new TreeDropdownField("ParentID", $this->fieldLabel('ParentID'), self::class, 'ID', 'MenuTitle')
2235
                    ),
2236
                    $visibility = new FieldGroup(
2237
                        new CheckboxField("ShowInMenus", $this->fieldLabel('ShowInMenus')),
2238
                        new CheckboxField("ShowInSearch", $this->fieldLabel('ShowInSearch'))
2239
                    ),
2240
                    $viewersOptionsField = new OptionsetField(
2241
                        "CanViewType",
2242
                        _t('SiteTree.ACCESSHEADER', "Who can view this page?")
2243
                    ),
2244
                    $viewerGroupsField = ListboxField::create("ViewerGroups", _t('SiteTree.VIEWERGROUPS', "Viewer Groups"))
2245
                        ->setSource($groupsMap)
2246
                        ->setAttribute(
2247
                            'data-placeholder',
2248
                            _t('SiteTree.GroupPlaceholder', 'Click to select group')
2249
                        ),
2250
                    $editorsOptionsField = new OptionsetField(
2251
                        "CanEditType",
2252
                        _t('SiteTree.EDITHEADER', "Who can edit this page?")
2253
                    ),
2254
                    $editorGroupsField = ListboxField::create("EditorGroups", _t('SiteTree.EDITORGROUPS', "Editor Groups"))
2255
                        ->setSource($groupsMap)
2256
                        ->setAttribute(
2257
                            'data-placeholder',
2258
                            _t('SiteTree.GroupPlaceholder', 'Click to select group')
2259
                        )
2260
                )
2261
            )
2262
        );
2263
2264
        $parentType->addExtraClass('noborder');
2265
        $visibility->setTitle($this->fieldLabel('Visibility'));
2266
2267
2268
        // This filter ensures that the ParentID dropdown selection does not show this node,
2269
        // or its descendents, as this causes vanishing bugs
2270
        $parentIDField->setFilterFunction(create_function('$node', "return \$node->ID != {$this->ID};"));
2271
        $parentTypeSelector->addExtraClass('parentTypeSelector');
2272
2273
        $tabBehaviour->setTitle(_t('SiteTree.TABBEHAVIOUR', "Behavior"));
2274
2275
        // Make page location fields read-only if the user doesn't have the appropriate permission
2276
        if (!Permission::check("SITETREE_REORGANISE")) {
2277
            $fields->makeFieldReadonly('ParentType');
2278
            if ($this->getParentType() === 'root') {
2279
                $fields->removeByName('ParentID');
2280
            } else {
2281
                $fields->makeFieldReadonly('ParentID');
2282
            }
2283
        }
2284
2285
        $viewersOptionsSource = array();
2286
        $viewersOptionsSource["Inherit"] = _t('SiteTree.INHERIT', "Inherit from parent page");
2287
        $viewersOptionsSource["Anyone"] = _t('SiteTree.ACCESSANYONE', "Anyone");
2288
        $viewersOptionsSource["LoggedInUsers"] = _t('SiteTree.ACCESSLOGGEDIN', "Logged-in users");
2289
        $viewersOptionsSource["OnlyTheseUsers"] = _t('SiteTree.ACCESSONLYTHESE', "Only these people (choose from list)");
2290
        $viewersOptionsField->setSource($viewersOptionsSource);
2291
2292
        $editorsOptionsSource = array();
2293
        $editorsOptionsSource["Inherit"] = _t('SiteTree.INHERIT', "Inherit from parent page");
2294
        $editorsOptionsSource["LoggedInUsers"] = _t('SiteTree.EDITANYONE', "Anyone who can log-in to the CMS");
2295
        $editorsOptionsSource["OnlyTheseUsers"] = _t('SiteTree.EDITONLYTHESE', "Only these people (choose from list)");
2296
        $editorsOptionsField->setSource($editorsOptionsSource);
2297
2298
        if (!Permission::check('SITETREE_GRANT_ACCESS')) {
2299
            $fields->makeFieldReadonly($viewersOptionsField);
2300
            if ($this->CanViewType == 'OnlyTheseUsers') {
2301
                $fields->makeFieldReadonly($viewerGroupsField);
2302
            } else {
2303
                $fields->removeByName('ViewerGroups');
2304
            }
2305
2306
            $fields->makeFieldReadonly($editorsOptionsField);
2307
            if ($this->CanEditType == 'OnlyTheseUsers') {
2308
                $fields->makeFieldReadonly($editorGroupsField);
2309
            } else {
2310
                $fields->removeByName('EditorGroups');
2311
            }
2312
        }
2313
2314
        if (self::$runCMSFieldsExtensions) {
2315
            $this->extend('updateSettingsFields', $fields);
2316
        }
2317
2318
        return $fields;
2319
    }
2320
2321
    /**
2322
     * @param bool $includerelations A boolean value to indicate if the labels returned should include relation fields
2323
     * @return array
2324
     */
2325
    public function fieldLabels($includerelations = true)
2326
    {
2327
        $cacheKey = static::class . '_' . $includerelations;
2328
        if (!isset(self::$_cache_field_labels[$cacheKey])) {
2329
            $labels = parent::fieldLabels($includerelations);
2330
            $labels['Title'] = _t('SiteTree.PAGETITLE', "Page name");
2331
            $labels['MenuTitle'] = _t('SiteTree.MENUTITLE', "Navigation label");
2332
            $labels['MetaDescription'] = _t('SiteTree.METADESC', "Meta Description");
2333
            $labels['ExtraMeta'] = _t('SiteTree.METAEXTRA', "Custom Meta Tags");
2334
            $labels['ClassName'] = _t('SiteTree.PAGETYPE', "Page type", 'Classname of a page object');
2335
            $labels['ParentType'] = _t('SiteTree.PARENTTYPE', "Page location");
2336
            $labels['ParentID'] = _t('SiteTree.PARENTID', "Parent page");
2337
            $labels['ShowInMenus'] =_t('SiteTree.SHOWINMENUS', "Show in menus?");
2338
            $labels['ShowInSearch'] = _t('SiteTree.SHOWINSEARCH', "Show in search?");
2339
            $labels['ViewerGroups'] = _t('SiteTree.VIEWERGROUPS', "Viewer Groups");
2340
            $labels['EditorGroups'] = _t('SiteTree.EDITORGROUPS', "Editor Groups");
2341
            $labels['URLSegment'] = _t('SiteTree.URLSegment', 'URL Segment', 'URL for this page');
2342
            $labels['Content'] = _t('SiteTree.Content', 'Content', 'Main HTML Content for a page');
2343
            $labels['CanViewType'] = _t('SiteTree.Viewers', 'Viewers Groups');
2344
            $labels['CanEditType'] = _t('SiteTree.Editors', 'Editors Groups');
2345
            $labels['Comments'] = _t('SiteTree.Comments', 'Comments');
2346
            $labels['Visibility'] = _t('SiteTree.Visibility', 'Visibility');
2347
            $labels['LinkChangeNote'] = _t(
2348
                'SiteTree.LINKCHANGENOTE',
2349
                'Changing this page\'s link will also affect the links of all child pages.'
2350
            );
2351
2352
            if ($includerelations) {
2353
                $labels['Parent'] = _t('SiteTree.has_one_Parent', 'Parent Page', 'The parent page in the site hierarchy');
2354
                $labels['LinkTracking'] = _t('SiteTree.many_many_LinkTracking', 'Link Tracking');
2355
                $labels['ImageTracking'] = _t('SiteTree.many_many_ImageTracking', 'Image Tracking');
2356
                $labels['BackLinkTracking'] = _t('SiteTree.many_many_BackLinkTracking', 'Backlink Tracking');
2357
            }
2358
2359
            self::$_cache_field_labels[$cacheKey] = $labels;
2360
        }
2361
2362
        return self::$_cache_field_labels[$cacheKey];
2363
    }
2364
2365
    /**
2366
     * Get the actions available in the CMS for this page - eg Save, Publish.
2367
     *
2368
     * Frontend scripts and styles know how to handle the following FormFields:
2369
     * - top-level FormActions appear as standalone buttons
2370
     * - top-level CompositeField with FormActions within appear as grouped buttons
2371
     * - TabSet & Tabs appear as a drop ups
2372
     * - FormActions within the Tab are restyled as links
2373
     * - major actions can provide alternate states for richer presentation (see ssui.button widget extension)
2374
     *
2375
     * @return FieldList The available actions for this page.
2376
     */
2377
    public function getCMSActions()
2378
    {
2379
        // Get status of page
2380
        $isOnDraft = $this->isOnDraft();
0 ignored issues
show
Documentation Bug introduced by
The method isOnDraft does not exist on object<SilverStripe\CMS\Model\SiteTree>? Since you implemented __call, maybe consider adding a @method annotation.

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

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

class ParentClass {
    private $data = array();

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

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

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
2381
        $isPublished = $this->isPublished();
0 ignored issues
show
Documentation Bug introduced by
The method isPublished does not exist on object<SilverStripe\CMS\Model\SiteTree>? Since you implemented __call, maybe consider adding a @method annotation.

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

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

class ParentClass {
    private $data = array();

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

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

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
2382
        $stagesDiffer = $this->stagesDiffer(Versioned::DRAFT, Versioned::LIVE);
0 ignored issues
show
Documentation Bug introduced by
The method stagesDiffer does not exist on object<SilverStripe\CMS\Model\SiteTree>? Since you implemented __call, maybe consider adding a @method annotation.

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

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

class ParentClass {
    private $data = array();

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

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

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
2383
2384
        // Check permissions
2385
        $canPublish = $this->canPublish();
2386
        $canUnpublish = $this->canUnpublish();
0 ignored issues
show
Documentation Bug introduced by
The method canUnpublish does not exist on object<SilverStripe\CMS\Model\SiteTree>? Since you implemented __call, maybe consider adding a @method annotation.

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

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

class ParentClass {
    private $data = array();

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

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

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
2387
        $canEdit = $this->canEdit();
2388
2389
        // Major actions appear as buttons immediately visible as page actions.
2390
        $majorActions = CompositeField::create()->setName('MajorActions');
2391
        $majorActions->setFieldHolderTemplate(get_class($majorActions) . '_holder_buttongroup');
2392
2393
        // Minor options are hidden behind a drop-up and appear as links (although they are still FormActions).
2394
        $rootTabSet = new TabSet('ActionMenus');
2395
        $moreOptions = new Tab(
2396
            'MoreOptions',
2397
            _t('SiteTree.MoreOptions', 'More options', 'Expands a view for more buttons')
2398
        );
2399
        $rootTabSet->push($moreOptions);
2400
        $rootTabSet->addExtraClass('ss-ui-action-tabset action-menus noborder');
2401
2402
        // Render page information into the "more-options" drop-up, on the top.
2403
        $liveRecord = Versioned::get_by_stage(self::class, Versioned::LIVE)->byID($this->ID);
2404
        $infoTemplate = SSViewer::get_templates_by_class(static::class, '_Information', self::class);
2405
        $moreOptions->push(
2406
            new LiteralField(
2407
                'Information',
2408
                $this->customise(array(
2409
                    'Live' => $liveRecord,
2410
                    'ExistsOnLive' => $isPublished
2411
                ))->renderWith($infoTemplate)
2412
            )
2413
        );
2414
2415
        // Add to campaign option if not-archived and has publish permission
2416
        if (($isPublished || $isOnDraft) && $canPublish) {
2417
            $moreOptions->push(
2418
                AddToCampaignHandler_FormAction::create()
2419
                    ->removeExtraClass('btn-primary')
2420
                    ->addExtraClass('btn-secondary')
2421
            );
2422
        }
2423
2424
        // "readonly"/viewing version that isn't the current version of the record
2425
        $stageRecord = Versioned::get_by_stage(static::class, Versioned::DRAFT)->byID($this->ID);
2426
        /** @skipUpgrade */
2427
        if ($stageRecord && $stageRecord->Version != $this->Version) {
0 ignored issues
show
Bug introduced by
The property Version does not seem to exist. Did you mean versioning?

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

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

Loading history...
2428
            $moreOptions->push(FormAction::create('email', _t('CMSMain.EMAIL', 'Email')));
2429
            $moreOptions->push(FormAction::create('rollback', _t('CMSMain.ROLLBACK', 'Roll back to this version')));
2430
            $actions = new FieldList(array($majorActions, $rootTabSet));
2431
2432
            // getCMSActions() can be extended with updateCMSActions() on a extension
2433
            $this->extend('updateCMSActions', $actions);
2434
            return $actions;
2435
        }
2436
2437
        // "unpublish"
2438
        if ($isPublished && $canPublish && $isOnDraft && $canUnpublish) {
2439
            $moreOptions->push(
2440
                FormAction::create('unpublish', _t('SiteTree.BUTTONUNPUBLISH', 'Unpublish'), 'delete')
2441
                    ->setDescription(_t('SiteTree.BUTTONUNPUBLISHDESC', 'Remove this page from the published site'))
2442
                    ->addExtraClass('btn-secondary')
2443
            );
2444
        }
2445
2446
        // "rollback"
2447
        if ($isOnDraft && $isPublished && $canEdit && $stagesDiffer) {
2448
            $moreOptions->push(
2449
                FormAction::create('rollback', _t('SiteTree.BUTTONCANCELDRAFT', 'Cancel draft changes'))
2450
                    ->setDescription(_t(
2451
                        'SiteTree.BUTTONCANCELDRAFTDESC',
2452
                        'Delete your draft and revert to the currently published page'
2453
                    ))
2454
                    ->addExtraClass('btn-secondary')
2455
            );
2456
        }
2457
2458
        // "restore"
2459
        if ($canEdit && !$isOnDraft && $isPublished) {
2460
            $majorActions->push(FormAction::create('revert', _t('CMSMain.RESTORE', 'Restore')));
2461
        }
2462
2463
        // Check if we can restore a deleted page
2464
        // Note: It would be nice to have a canRestore() permission at some point
2465
        if ($canEdit && !$isOnDraft && !$isPublished) {
2466
            // Determine if we should force a restore to root (where once it was a subpage)
2467
            $restoreToRoot = $this->isParentArchived();
2468
2469
            // "restore"
2470
            $title = $restoreToRoot
2471
                ? _t('CMSMain.RESTORE_TO_ROOT', 'Restore draft at top level')
2472
                : _t('CMSMain.RESTORE', 'Restore draft');
2473
            $description = $restoreToRoot
2474
                ? _t('CMSMain.RESTORE_TO_ROOT_DESC', 'Restore the archived version to draft as a top level page')
2475
                : _t('CMSMain.RESTORE_DESC', 'Restore the archived version to draft');
2476
            $majorActions->push(
2477
                FormAction::create('restore', $title)
2478
                    ->setDescription($description)
2479
                    ->setAttribute('data-to-root', $restoreToRoot)
0 ignored issues
show
Documentation introduced by
$restoreToRoot is of type boolean, but the function expects a string.

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
2480
                    ->setAttribute('data-icon', 'decline')
2481
            );
2482
        }
2483
2484
        // If a page is on any stage it can be archived
2485
        if (($isOnDraft || $isPublished) && $this->canArchive()) {
0 ignored issues
show
Documentation Bug introduced by
The method canArchive does not exist on object<SilverStripe\CMS\Model\SiteTree>? Since you implemented __call, maybe consider adding a @method annotation.

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

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

class ParentClass {
    private $data = array();

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

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

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
2486
            $title = $isPublished
2487
                ? _t('CMSMain.UNPUBLISH_AND_ARCHIVE', 'Unpublish and archive')
2488
                : _t('CMSMain.ARCHIVE', 'Archive');
2489
            $moreOptions->push(
2490
                FormAction::create('archive', $title)
2491
                    ->addExtraClass('delete btn btn-secondary')
2492
                    ->setDescription(_t(
2493
                        'SiteTree.BUTTONDELETEDESC',
2494
                        'Remove from draft/live and send to archive'
2495
                    ))
2496
            );
2497
        }
2498
2499
        // "save", supports an alternate state that is still clickable, but notifies the user that the action is not needed.
2500
        if ($canEdit && $isOnDraft) {
2501
            $majorActions->push(
2502
                FormAction::create('save', _t('SiteTree.BUTTONSAVED', 'Saved'))
2503
                    ->addExtraClass('btn-secondary-outline font-icon-check-mark')
2504
                    ->setAttribute('data-btn-alternate', 'btn action btn-primary font-icon-save')
2505
                    ->setUseButtonTag(true)
2506
                    ->setAttribute('data-text-alternate', _t('CMSMain.SAVEDRAFT', 'Save draft'))
2507
            );
2508
        }
2509
2510
        if ($canPublish && $isOnDraft) {
2511
            // "publish", as with "save", it supports an alternate state to show when action is needed.
2512
            $majorActions->push(
2513
                $publish = FormAction::create('publish', _t('SiteTree.BUTTONPUBLISHED', 'Published'))
2514
                    ->addExtraClass('btn-secondary-outline font-icon-check-mark')
2515
                    ->setAttribute('data-btn-alternate', 'btn action btn-primary font-icon-rocket')
2516
                    ->setUseButtonTag(true)
2517
                    ->setAttribute('data-text-alternate', _t('SiteTree.BUTTONSAVEPUBLISH', 'Save & publish'))
2518
            );
2519
2520
            // Set up the initial state of the button to reflect the state of the underlying SiteTree object.
2521
            if ($stagesDiffer) {
2522
                $publish->addExtraClass('btn-primary font-icon-rocket');
2523
                $publish->setTitle(_t('SiteTree.BUTTONSAVEPUBLISH', 'Save & publish'));
2524
                $publish->removeExtraClass('btn-secondary-outline font-icon-check-mark');
2525
            }
2526
        }
2527
2528
        $actions = new FieldList(array($majorActions, $rootTabSet));
2529
2530
        // Hook for extensions to add/remove actions.
2531
        $this->extend('updateCMSActions', $actions);
2532
2533
        return $actions;
2534
    }
2535
2536
    public function onAfterPublish()
2537
    {
2538
        // Force live sort order to match stage sort order
2539
        DB::prepared_query(
2540
            'UPDATE "SiteTree_Live"
2541
			SET "Sort" = (SELECT "SiteTree"."Sort" FROM "SiteTree" WHERE "SiteTree_Live"."ID" = "SiteTree"."ID")
2542
			WHERE EXISTS (SELECT "SiteTree"."Sort" FROM "SiteTree" WHERE "SiteTree_Live"."ID" = "SiteTree"."ID") AND "ParentID" = ?',
2543
            array($this->ParentID)
0 ignored issues
show
Documentation introduced by
The property ParentID does not exist on object<SilverStripe\CMS\Model\SiteTree>. Since you implemented __get, maybe consider adding a @property annotation.

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

<?php

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

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

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

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

}

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

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

See also the PhpDoc documentation for @property.

Loading history...
2544
        );
2545
    }
2546
2547
    /**
2548
     * Update draft dependant pages
2549
     */
2550
    public function onAfterRevertToLive()
2551
    {
2552
        // Use an alias to get the updates made by $this->publish
2553
        /** @var SiteTree $stageSelf */
2554
        $stageSelf = Versioned::get_by_stage(self::class, Versioned::DRAFT)->byID($this->ID);
2555
        $stageSelf->writeWithoutVersion();
0 ignored issues
show
Documentation Bug introduced by
The method writeWithoutVersion does not exist on object<SilverStripe\CMS\Model\SiteTree>? Since you implemented __call, maybe consider adding a @method annotation.

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

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

class ParentClass {
    private $data = array();

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

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

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
2556
2557
        // Need to update pages linking to this one as no longer broken
2558
        foreach ($stageSelf->DependentPages() as $page) {
2559
            /** @var SiteTree $page */
2560
            $page->writeWithoutVersion();
0 ignored issues
show
Documentation Bug introduced by
The method writeWithoutVersion does not exist on object<SilverStripe\CMS\Model\SiteTree>? Since you implemented __call, maybe consider adding a @method annotation.

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

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

class ParentClass {
    private $data = array();

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

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

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
2561
        }
2562
    }
2563
2564
    /**
2565
     * Determine if this page references a parent which is archived, and not available in stage
2566
     *
2567
     * @return bool True if there is an archived parent
2568
     */
2569
    protected function isParentArchived()
2570
    {
2571
        if ($parentID = $this->ParentID) {
0 ignored issues
show
Documentation introduced by
The property ParentID does not exist on object<SilverStripe\CMS\Model\SiteTree>. Since you implemented __set, maybe consider adding a @property annotation.

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

<?php

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

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

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

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

}

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

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

See also the PhpDoc documentation for @property.

Loading history...
2572
            /** @var SiteTree $parentPage */
2573
            $parentPage = Versioned::get_latest_version(self::class, $parentID);
2574
            if (!$parentPage || !$parentPage->isOnDraft()) {
0 ignored issues
show
Documentation Bug introduced by
The method isOnDraft does not exist on object<SilverStripe\CMS\Model\SiteTree>? Since you implemented __call, maybe consider adding a @method annotation.

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

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

class ParentClass {
    private $data = array();

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

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

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
2575
                return true;
2576
            }
2577
        }
2578
        return false;
2579
    }
2580
2581
    /**
2582
     * Restore the content in the active copy of this SiteTree page to the stage site.
2583
     *
2584
     * @return self
2585
     */
2586
    public function doRestoreToStage()
2587
    {
2588
        $this->invokeWithExtensions('onBeforeRestoreToStage', $this);
2589
2590
        // Ensure that the parent page is restored, otherwise restore to root
2591
        if ($this->isParentArchived()) {
2592
            $this->ParentID = 0;
0 ignored issues
show
Documentation introduced by
The property ParentID does not exist on object<SilverStripe\CMS\Model\SiteTree>. Since you implemented __set, maybe consider adding a @property annotation.

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

<?php

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

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

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

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

}

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

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

See also the PhpDoc documentation for @property.

Loading history...
2593
        }
2594
2595
        // if no record can be found on draft stage (meaning it has been "deleted from draft" before),
2596
        // create an empty record
2597
        if (!DB::prepared_query("SELECT \"ID\" FROM \"SiteTree\" WHERE \"ID\" = ?", array($this->ID))->value()) {
2598
            $conn = DB::get_conn();
2599
            if (method_exists($conn, 'allowPrimaryKeyEditing')) {
2600
                $conn->allowPrimaryKeyEditing(self::class, true);
0 ignored issues
show
Bug introduced by
The method allowPrimaryKeyEditing() does not seem to exist on object<SilverStripe\ORM\Connect\Database>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
2601
            }
2602
            DB::prepared_query("INSERT INTO \"SiteTree\" (\"ID\") VALUES (?)", array($this->ID));
2603
            if (method_exists($conn, 'allowPrimaryKeyEditing')) {
2604
                $conn->allowPrimaryKeyEditing(self::class, false);
0 ignored issues
show
Bug introduced by
The method allowPrimaryKeyEditing() does not seem to exist on object<SilverStripe\ORM\Connect\Database>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
2605
            }
2606
        }
2607
2608
        $oldReadingMode = Versioned::get_reading_mode();
2609
        Versioned::set_stage(Versioned::DRAFT);
2610
        $this->forceChange();
2611
        $this->write();
2612
2613
        /** @var SiteTree $result */
2614
        $result = DataObject::get_by_id(self::class, $this->ID);
2615
2616
        // Need to update pages linking to this one as no longer broken
2617
        foreach ($result->DependentPages(false) as $page) {
2618
            // $page->write() calls syncLinkTracking, which does all the hard work for us.
2619
            $page->write();
2620
        }
2621
2622
        Versioned::set_reading_mode($oldReadingMode);
2623
2624
        $this->invokeWithExtensions('onAfterRestoreToStage', $this);
2625
2626
        return $result;
2627
    }
2628
2629
    /**
2630
     * Check if this page is new - that is, if it has yet to have been written to the database.
2631
     *
2632
     * @return bool
2633
     */
2634
    public function isNew()
2635
    {
2636
        /**
2637
         * This check was a problem for a self-hosted site, and may indicate a bug in the interpreter on their server,
2638
         * or a bug here. Changing the condition from empty($this->ID) to !$this->ID && !$this->record['ID'] fixed this.
2639
         */
2640
        if (empty($this->ID)) {
2641
            return true;
2642
        }
2643
2644
        if (is_numeric($this->ID)) {
2645
            return false;
2646
        }
2647
2648
        return stripos($this->ID, 'new') === 0;
2649
    }
2650
2651
    /**
2652
     * Get the class dropdown used in the CMS to change the class of a page. This returns the list of options in the
2653
     * dropdown as a Map from class name to singular name. Filters by {@link SiteTree->canCreate()}, as well as
2654
     * {@link SiteTree::$needs_permission}.
2655
     *
2656
     * @return array
2657
     */
2658
    protected function getClassDropdown()
2659
    {
2660
        $classes = self::page_type_classes();
2661
        $currentClass = null;
2662
2663
        $result = array();
2664
        foreach ($classes as $class) {
2665
            $instance = singleton($class);
2666
2667
            // if the current page type is this the same as the class type always show the page type in the list
2668
            if ($this->ClassName != $instance->ClassName) {
2669
                if ($instance instanceof HiddenClass) {
2670
                    continue;
2671
                }
2672
                if (!$instance->canCreate(null, array('Parent' => $this->ParentID ? $this->Parent() : null))) {
0 ignored issues
show
Documentation introduced by
The property ParentID does not exist on object<SilverStripe\CMS\Model\SiteTree>. Since you implemented __get, maybe consider adding a @property annotation.

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

<?php

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

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

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

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

}

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

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

See also the PhpDoc documentation for @property.

Loading history...
2673
                    continue;
2674
                }
2675
            }
2676
2677
            if ($perms = $instance->stat('need_permission')) {
2678
                if (!$this->can($perms)) {
2679
                    continue;
2680
                }
2681
            }
2682
2683
            $pageTypeName = $instance->i18n_singular_name();
2684
2685
            $currentClass = $class;
2686
            $result[$class] = $pageTypeName;
2687
2688
            // If we're in translation mode, the link between the translated pagetype title and the actual classname
2689
            // might not be obvious, so we add it in parantheses. Example: class "RedirectorPage" has the title
2690
            // "Weiterleitung" in German, so it shows up as "Weiterleitung (RedirectorPage)"
2691
            if (i18n::getData()->langFromLocale(i18n::get_locale()) != 'en') {
2692
                $result[$class] = $result[$class] .  " ({$class})";
2693
            }
2694
        }
2695
2696
        // sort alphabetically, and put current on top
2697
        asort($result);
2698
        if ($currentClass) {
2699
            $currentPageTypeName = $result[$currentClass];
2700
            unset($result[$currentClass]);
2701
            $result = array_reverse($result);
2702
            $result[$currentClass] = $currentPageTypeName;
2703
            $result = array_reverse($result);
2704
        }
2705
2706
        return $result;
2707
    }
2708
2709
    /**
2710
     * Returns an array of the class names of classes that are allowed to be children of this class.
2711
     *
2712
     * @return string[]
2713
     */
2714
    public function allowedChildren()
2715
    {
2716
        // Get config based on old FIRST_SET rules
2717
        $candidates = null;
2718
        $class = get_class($this);
2719
        while ($class) {
2720
            if (Config::inst()->exists($class, 'allowed_children', Config::UNINHERITED)) {
2721
                $candidates = Config::inst()->get($class, 'allowed_children', Config::UNINHERITED);
2722
                break;
2723
            }
2724
            $class = get_parent_class($class);
2725
        }
2726
        if (!$candidates || $candidates === 'none' || $candidates === 'SiteTree_root') {
2727
            return [];
2728
        }
2729
2730
        // Parse candidate list
2731
        $allowedChildren = [];
2732
        foreach ($candidates as $candidate) {
2733
            // If a classname is prefixed by "*", such as "*Page", then only that class is allowed - no subclasses.
2734
            // Otherwise, the class and all its subclasses are allowed.
2735
            if (substr($candidate, 0, 1) == '*') {
2736
                $allowedChildren[] = substr($candidate, 1);
2737
            } elseif ($subclasses = ClassInfo::subclassesFor($candidate)) {
2738
                foreach ($subclasses as $subclass) {
2739
                    if ($subclass == 'SiteTree_root' || singleton($subclass) instanceof HiddenClass) {
2740
                        continue;
2741
                    }
2742
                    $allowedChildren[] = $subclass;
2743
                }
2744
            }
2745
        }
2746
        return $allowedChildren;
2747
    }
2748
2749
    /**
2750
     * Returns the class name of the default class for children of this page.
2751
     *
2752
     * @return string
2753
     */
2754
    public function defaultChild()
2755
    {
2756
        $default = $this->stat('default_child');
2757
        $allowed = $this->allowedChildren();
2758
        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...
2759
            if (!$default || !in_array($default, $allowed)) {
2760
                $default = reset($allowed);
2761
            }
2762
            return $default;
2763
        }
2764
        return null;
2765
    }
2766
2767
    /**
2768
     * Returns the class name of the default class for the parent of this page.
2769
     *
2770
     * @return string
2771
     */
2772
    public function defaultParent()
2773
    {
2774
        return $this->stat('default_parent');
2775
    }
2776
2777
    /**
2778
     * Get the title for use in menus for this page. If the MenuTitle field is set it returns that, else it returns the
2779
     * Title field.
2780
     *
2781
     * @return string
2782
     */
2783
    public function getMenuTitle()
2784
    {
2785
        if ($value = $this->getField("MenuTitle")) {
2786
            return $value;
2787
        } else {
2788
            return $this->getField("Title");
2789
        }
2790
    }
2791
2792
2793
    /**
2794
     * Set the menu title for this page.
2795
     *
2796
     * @param string $value
2797
     */
2798
    public function setMenuTitle($value)
2799
    {
2800
        if ($value == $this->getField("Title")) {
2801
            $this->setField("MenuTitle", null);
2802
        } else {
2803
            $this->setField("MenuTitle", $value);
2804
        }
2805
    }
2806
2807
    /**
2808
     * A flag provides the user with additional data about the current page status, for example a "removed from draft"
2809
     * status. Each page can have more than one status flag. Returns a map of a unique key to a (localized) title for
2810
     * the flag. The unique key can be reused as a CSS class. Use the 'updateStatusFlags' extension point to customize
2811
     * the flags.
2812
     *
2813
     * Example (simple):
2814
     *   "deletedonlive" => "Deleted"
2815
     *
2816
     * Example (with optional title attribute):
2817
     *   "deletedonlive" => array('text' => "Deleted", 'title' => 'This page has been deleted')
2818
     *
2819
     * @param bool $cached Whether to serve the fields from cache; false regenerate them
2820
     * @return array
2821
     */
2822
    public function getStatusFlags($cached = true)
2823
    {
2824
        if (!$this->_cache_statusFlags || !$cached) {
2825
            $flags = array();
2826
            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...
2827
                $flags['removedfromdraft'] = array(
2828
                    'text' => _t('SiteTree.ONLIVEONLYSHORT', 'On live only'),
2829
                    'title' => _t('SiteTree.ONLIVEONLYSHORTHELP', 'Page is published, but has been deleted from draft'),
2830
                );
2831
            } 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...
2832
                $flags['archived'] = array(
2833
                    'text' => _t('SiteTree.ARCHIVEDPAGESHORT', 'Archived'),
2834
                    'title' => _t('SiteTree.ARCHIVEDPAGEHELP', 'Page is removed from draft and live'),
2835
                );
2836
            } 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...
2837
                $flags['addedtodraft'] = array(
2838
                    'text' => _t('SiteTree.ADDEDTODRAFTSHORT', 'Draft'),
2839
                    'title' => _t('SiteTree.ADDEDTODRAFTHELP', "Page has not been published yet")
2840
                );
2841
            } 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...
2842
                $flags['modified'] = array(
2843
                    'text' => _t('SiteTree.MODIFIEDONDRAFTSHORT', 'Modified'),
2844
                    'title' => _t('SiteTree.MODIFIEDONDRAFTHELP', 'Page has unpublished changes'),
2845
                );
2846
            }
2847
2848
            $this->extend('updateStatusFlags', $flags);
2849
2850
            $this->_cache_statusFlags = $flags;
2851
        }
2852
2853
        return $this->_cache_statusFlags;
2854
    }
2855
2856
    /**
2857
     * getTreeTitle will return three <span> html DOM elements, an empty <span> with the class 'jstree-pageicon' in
2858
     * front, following by a <span> wrapping around its MenutTitle, then following by a <span> indicating its
2859
     * publication status.
2860
     *
2861
     * @return string An HTML string ready to be directly used in a template
2862
     */
2863
    public function getTreeTitle()
2864
    {
2865
        // Build the list of candidate children
2866
        $children = array();
2867
        $candidates = static::page_type_classes();
2868
        foreach ($this->allowedChildren() as $childClass) {
2869
            if (!in_array($childClass, $candidates)) {
2870
                continue;
2871
            }
2872
            $child = singleton($childClass);
2873
            if ($child->canCreate(null, array('Parent' => $this))) {
2874
                $children[$childClass] = $child->i18n_singular_name();
2875
            }
2876
        }
2877
        $flags = $this->getStatusFlags();
2878
        $treeTitle = sprintf(
2879
            "<span class=\"jstree-pageicon\"></span><span class=\"item\" data-allowedchildren=\"%s\">%s</span>",
2880
            Convert::raw2att(Convert::raw2json($children)),
2881
            Convert::raw2xml(str_replace(array("\n","\r"), "", $this->MenuTitle))
2882
        );
2883
        foreach ($flags as $class => $data) {
2884
            if (is_string($data)) {
2885
                $data = array('text' => $data);
2886
            }
2887
            $treeTitle .= sprintf(
2888
                "<span class=\"badge %s\"%s>%s</span>",
2889
                'status-' . Convert::raw2xml($class),
2890
                (isset($data['title'])) ? sprintf(' title="%s"', Convert::raw2xml($data['title'])) : '',
2891
                Convert::raw2xml($data['text'])
2892
            );
2893
        }
2894
2895
        return $treeTitle;
2896
    }
2897
2898
    /**
2899
     * Returns the page in the current page stack of the given level. Level(1) will return the main menu item that
2900
     * we're currently inside, etc.
2901
     *
2902
     * @param int $level
2903
     * @return SiteTree
2904
     */
2905
    public function Level($level)
2906
    {
2907
        $parent = $this;
2908
        $stack = array($parent);
2909
        while (($parent = $parent->Parent()) && $parent->exists()) {
2910
            array_unshift($stack, $parent);
2911
        }
2912
2913
        return isset($stack[$level-1]) ? $stack[$level-1] : null;
2914
    }
2915
2916
    /**
2917
     * Gets the depth of this page in the sitetree, where 1 is the root level
2918
     *
2919
     * @return int
2920
     */
2921
    public function getPageLevel()
2922
    {
2923
        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...
2924
            return 1 + $this->Parent()->getPageLevel();
2925
        }
2926
        return 1;
2927
    }
2928
2929
    /**
2930
     * Find the controller name by our convention of {$ModelClass}Controller
2931
     *
2932
     * @return string
2933
     */
2934
    public function getControllerName()
2935
    {
2936
        //default controller for SiteTree objects
2937
        $controller = ContentController::class;
2938
2939
        //go through the ancestry for this class looking for
2940
        $ancestry = ClassInfo::ancestry(static::class);
2941
        // loop over the array going from the deepest descendant (ie: the current class) to SiteTree
2942
        while ($class = array_pop($ancestry)) {
2943
            //we don't need to go any deeper than the SiteTree class
2944
            if ($class == SiteTree::class) {
2945
                break;
2946
            }
2947
            // If we have a class of "{$ClassName}Controller" then we found our controller
2948
            if (class_exists($candidate = sprintf('%sController', $class))) {
2949
                $controller = $candidate;
2950
                break;
2951
            } elseif (class_exists($candidate = sprintf('%s_Controller', $class))) {
2952
                // Support the legacy underscored filename, but raise a deprecation notice
2953
                Deprecation::notice(
2954
                    '5.0',
2955
                    'Underscored controller class names are deprecated. Use "MyController" instead of "My_Controller".',
2956
                    Deprecation::SCOPE_GLOBAL
2957
                );
2958
                $controller = $candidate;
2959
                break;
2960
            }
2961
        }
2962
2963
        return $controller;
2964
    }
2965
2966
    /**
2967
     * Return the CSS classes to apply to this node in the CMS tree.
2968
     *
2969
     * @param string $numChildrenMethod
2970
     * @return string
2971
     */
2972
    public function CMSTreeClasses($numChildrenMethod = "numChildren")
2973
    {
2974
        $classes = sprintf('class-%s', static::class);
2975
        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...
2976
            $classes .= " BrokenLink";
2977
        }
2978
2979
        if (!$this->canAddChildren()) {
2980
            $classes .= " nochildren";
2981
        }
2982
2983
        if (!$this->canEdit() && !$this->canAddChildren()) {
2984
            if (!$this->canView()) {
2985
                $classes .= " disabled";
2986
            } else {
2987
                $classes .= " edit-disabled";
2988
            }
2989
        }
2990
2991
        if (!$this->ShowInMenus) {
2992
            $classes .= " notinmenu";
2993
        }
2994
2995
        //TODO: Add integration
2996
        /*
2997
		if($this->hasExtension('Translatable') && $controller->Locale != Translatable::default_locale() && !$this->isTranslation())
2998
			$classes .= " untranslated ";
2999
		*/
3000
        $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...
3001
3002
        return $classes;
3003
    }
3004
3005
    /**
3006
     * Stops extendCMSFields() being called on getCMSFields(). This is useful when you need access to fields added by
3007
     * subclasses of SiteTree in a extension. Call before calling parent::getCMSFields(), and reenable afterwards.
3008
     */
3009
    public static function disableCMSFieldsExtensions()
3010
    {
3011
        self::$runCMSFieldsExtensions = false;
3012
    }
3013
3014
    /**
3015
     * Reenables extendCMSFields() being called on getCMSFields() after it has been disabled by
3016
     * disableCMSFieldsExtensions().
3017
     */
3018
    public static function enableCMSFieldsExtensions()
3019
    {
3020
        self::$runCMSFieldsExtensions = true;
3021
    }
3022
3023
    public function providePermissions()
3024
    {
3025
        return array(
3026
            'SITETREE_GRANT_ACCESS' => array(
3027
                'name' => _t('SiteTree.PERMISSION_GRANTACCESS_DESCRIPTION', 'Manage access rights for content'),
3028
                'help' => _t('SiteTree.PERMISSION_GRANTACCESS_HELP', 'Allow setting of page-specific access restrictions in the "Pages" section.'),
3029
                'category' => _t('Permissions.PERMISSIONS_CATEGORY', 'Roles and access permissions'),
3030
                'sort' => 100
3031
            ),
3032
            'SITETREE_VIEW_ALL' => array(
3033
                'name' => _t('SiteTree.VIEW_ALL_DESCRIPTION', 'View any page'),
3034
                'category' => _t('Permissions.CONTENT_CATEGORY', 'Content permissions'),
3035
                'sort' => -100,
3036
                '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')
3037
            ),
3038
            'SITETREE_EDIT_ALL' => array(
3039
                'name' => _t('SiteTree.EDIT_ALL_DESCRIPTION', 'Edit any page'),
3040
                'category' => _t('Permissions.CONTENT_CATEGORY', 'Content permissions'),
3041
                'sort' => -50,
3042
                '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')
3043
            ),
3044
            'SITETREE_REORGANISE' => array(
3045
                'name' => _t('SiteTree.REORGANISE_DESCRIPTION', 'Change site structure'),
3046
                'category' => _t('Permissions.CONTENT_CATEGORY', 'Content permissions'),
3047
                'help' => _t('SiteTree.REORGANISE_HELP', 'Rearrange pages in the site tree through drag&drop.'),
3048
                'sort' => 100
3049
            ),
3050
            'VIEW_DRAFT_CONTENT' => array(
3051
                'name' => _t('SiteTree.VIEW_DRAFT_CONTENT', 'View draft content'),
3052
                'category' => _t('Permissions.CONTENT_CATEGORY', 'Content permissions'),
3053
                '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.'),
3054
                'sort' => 100
3055
            )
3056
        );
3057
    }
3058
3059
    /**
3060
     * Default singular name for page / sitetree
3061
     *
3062
     * @return string
3063
     */
3064 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...
3065
    {
3066
        $base = in_array(static::class, [Page::class, self::class]);
3067
        if ($base) {
3068
            return $this->stat('base_singular_name');
3069
        }
3070
        return parent::singular_name();
3071
    }
3072
3073
    /**
3074
     * Default plural name for page / sitetree
3075
     *
3076
     * @return string
3077
     */
3078 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...
3079
    {
3080
        $base = in_array(static::class, [Page::class, self::class]);
3081
        if ($base) {
3082
            return $this->stat('base_plural_name');
3083
        }
3084
        return parent::plural_name();
3085
    }
3086
3087
    /**
3088
     * Get description for this page type
3089
     *
3090
     * @return string|null
3091
     */
3092
    public function classDescription()
3093
    {
3094
        $base = in_array(static::class, [Page::class, self::class]);
3095
        if ($base) {
3096
            return $this->stat('base_description');
3097
        }
3098
        return $this->stat('description');
3099
    }
3100
3101
    /**
3102
     * Get localised description for this page
3103
     *
3104
     * @return string|null
3105
     */
3106
    public function i18n_classDescription()
3107
    {
3108
        $description = $this->classDescription();
3109
        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...
3110
            return _t(static::class.'.DESCRIPTION', $description);
3111
        }
3112
        return null;
3113
    }
3114
3115
    /**
3116
     * Overloaded to also provide entities for 'Page' class which is usually located in custom code, hence textcollector
3117
     * picks it up for the wrong folder.
3118
     *
3119
     * @return array
3120
     */
3121
    public function provideI18nEntities()
3122
    {
3123
        $entities = parent::provideI18nEntities();
3124
3125
        // Add optional description
3126
        $description = $this->classDescription();
3127
        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...
3128
            $entities[static::class . '.DESCRIPTION'] = $description;
3129
        }
3130
        return $entities;
3131
    }
3132
3133
    /**
3134
     * Returns 'root' if the current page has no parent, or 'subpage' otherwise
3135
     *
3136
     * @return string
3137
     */
3138
    public function getParentType()
3139
    {
3140
        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...
3141
    }
3142
3143
    /**
3144
     * Clear the permissions cache for SiteTree
3145
     */
3146
    public static function reset()
3147
    {
3148
        self::$cache_permissions = array();
3149
    }
3150
3151
    public static function on_db_reset()
3152
    {
3153
        self::$cache_permissions = array();
3154
    }
3155
}
3156