Completed
Pull Request — master (#1688)
by Sam
15:41
created

SiteTree::getMimeType()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

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

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
583
            '"SiteTree"."ID"' => $this->ID
584
        ));
585
        if ($live) {
586
            $link = $live->AbsoluteLink();
587
            if ($includeStageEqualsLive) {
588
                $link = Controller::join_links($link, '?stage=Live');
589
            }
590
        } else {
591
            $link = null;
592
        }
593
594
        Versioned::set_reading_mode($oldReadingMode);
595
        return $link;
596
    }
597
598
    /**
599
     * Generates a link to edit this page in the CMS.
600
     *
601
     * @return string
602
     */
603
    public function CMSEditLink()
604
    {
605
        $link = Controller::join_links(
606
            CMSPageEditController::singleton()->Link('show'),
607
            $this->ID
608
        );
609
        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 609 which is incompatible with the return type declared by the interface SilverStripe\Admin\CMSPreviewable::CMSEditLink of type string. It seems like you forgot to handle an error condition.
Loading history...
610
    }
611
612
613
    /**
614
     * Return a CSS identifier generated from this page's link.
615
     *
616
     * @return string The URL segment
617
     */
618
    public function ElementName()
619
    {
620
        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...
621
    }
622
623
    /**
624
     * Returns true if this is the currently active page being used to handle this request.
625
     *
626
     * @return bool
627
     */
628
    public function isCurrent()
629
    {
630
        $currentPage = Director::get_current_page();
631
        if ($currentPage instanceof ContentController) {
632
            $currentPage = $currentPage->data();
633
        }
634
        if ($currentPage instanceof SiteTree) {
635
            return $currentPage === $this || $currentPage->ID === $this->ID;
636
        }
637
        return false;
638
    }
639
640
    /**
641
     * Check if this page is in the currently active section (e.g. it is either current or one of its children is
642
     * currently being viewed).
643
     *
644
     * @return bool
645
     */
646
    public function isSection()
647
    {
648
        return $this->isCurrent() || (
649
            Director::get_current_page() instanceof SiteTree && in_array($this->ID, Director::get_current_page()->getAncestors()->column())
650
        );
651
    }
652
653
    /**
654
     * Check if the parent of this page has been removed (or made otherwise unavailable), and is still referenced by
655
     * this child. Any such orphaned page may still require access via the CMS, but should not be shown as accessible
656
     * to external users.
657
     *
658
     * @return bool
659
     */
660
    public function isOrphaned()
661
    {
662
        // Always false for root pages
663
        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...
664
            return false;
665
        }
666
667
        // Parent must exist and not be an orphan itself
668
        $parent = $this->Parent();
669
        return !$parent || !$parent->exists() || $parent->isOrphaned();
670
    }
671
672
    /**
673
     * Return "link" or "current" depending on if this is the {@link SiteTree::isCurrent()} current page.
674
     *
675
     * @return string
676
     */
677
    public function LinkOrCurrent()
678
    {
679
        return $this->isCurrent() ? 'current' : 'link';
680
    }
681
682
    /**
683
     * Return "link" or "section" depending on if this is the {@link SiteTree::isSeciton()} current section.
684
     *
685
     * @return string
686
     */
687
    public function LinkOrSection()
688
    {
689
        return $this->isSection() ? 'section' : 'link';
690
    }
691
692
    /**
693
     * Return "link", "current" or "section" depending on if this page is the current page, or not on the current page
694
     * but in the current section.
695
     *
696
     * @return string
697
     */
698
    public function LinkingMode()
699
    {
700
        if ($this->isCurrent()) {
701
            return 'current';
702
        } elseif ($this->isSection()) {
703
            return 'section';
704
        } else {
705
            return 'link';
706
        }
707
    }
708
709
    /**
710
     * Check if this page is in the given current section.
711
     *
712
     * @param string $sectionName Name of the section to check
713
     * @return bool True if we are in the given section
714
     */
715
    public function InSection($sectionName)
716
    {
717
        $page = Director::get_current_page();
718
        while ($page && $page->exists()) {
719
            if ($sectionName == $page->URLSegment) {
720
                return true;
721
            }
722
            $page = $page->Parent();
723
        }
724
        return false;
725
    }
726
727
    /**
728
     * Reset Sort on duped page
729
     *
730
     * @param SiteTree $original
731
     * @param bool $doWrite
732
     */
733
    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...
734
    {
735
        $this->Sort = 0;
736
    }
737
738
    /**
739
     * Duplicates each child of this node recursively and returns the top-level duplicate node.
740
     *
741
     * @return static The duplicated object
742
     */
743
    public function duplicateWithChildren()
744
    {
745
        /** @var SiteTree $clone */
746
        $clone = $this->duplicate();
747
        $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...
748
749
        if ($children) {
750
            /** @var SiteTree $child */
751
            $sort = 0;
752
            foreach ($children as $child) {
753
                $childClone = $child->duplicateWithChildren();
754
                $childClone->ParentID = $clone->ID;
755
                //retain sort order by manually setting sort values
756
                $childClone->Sort = ++$sort;
757
                $childClone->write();
758
            }
759
        }
760
761
        return $clone;
762
    }
763
764
    /**
765
     * Duplicate this node and its children as a child of the node with the given ID
766
     *
767
     * @param int $id ID of the new node's new parent
768
     */
769
    public function duplicateAsChild($id)
770
    {
771
        /** @var SiteTree $newSiteTree */
772
        $newSiteTree = $this->duplicate();
773
        $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...
774
        $newSiteTree->Sort = 0;
775
        $newSiteTree->write();
776
    }
777
778
    /**
779
     * Return a breadcrumb trail to this page. Excludes "hidden" pages (with ShowInMenus=0) by default.
780
     *
781
     * @param int $maxDepth The maximum depth to traverse.
782
     * @param boolean $unlinked Whether to link page titles.
783
     * @param boolean|string $stopAtPageType ClassName of a page to stop the upwards traversal.
784
     * @param boolean $showHidden Include pages marked with the attribute ShowInMenus = 0
785
     * @return string The breadcrumb trail.
786
     */
787
    public function Breadcrumbs($maxDepth = 20, $unlinked = false, $stopAtPageType = false, $showHidden = false)
788
    {
789
        $pages = $this->getBreadcrumbItems($maxDepth, $stopAtPageType, $showHidden);
790
        $template = new SSViewer('BreadcrumbsTemplate');
791
        return $template->process($this->customise(new ArrayData(array(
792
            "Pages" => $pages,
793
            "Unlinked" => $unlinked
794
        ))));
795
    }
796
797
798
    /**
799
     * Returns a list of breadcrumbs for the current page.
800
     *
801
     * @param int $maxDepth The maximum depth to traverse.
802
     * @param boolean|string $stopAtPageType ClassName of a page to stop the upwards traversal.
803
     * @param boolean $showHidden Include pages marked with the attribute ShowInMenus = 0
804
     *
805
     * @return ArrayList
806
    */
807
    public function getBreadcrumbItems($maxDepth = 20, $stopAtPageType = false, $showHidden = false)
808
    {
809
        $page = $this;
810
        $pages = array();
811
812
        while ($page
813
            && $page->exists()
814
            && (!$maxDepth || count($pages) < $maxDepth)
815
            && (!$stopAtPageType || $page->ClassName != $stopAtPageType)
816
        ) {
817
            if ($showHidden || $page->ShowInMenus || ($page->ID == $this->ID)) {
818
                $pages[] = $page;
819
            }
820
821
            $page = $page->Parent();
822
        }
823
824
        return new ArrayList(array_reverse($pages));
825
    }
826
827
828
    /**
829
     * Make this page a child of another page.
830
     *
831
     * If the parent page does not exist, resolve it to a valid ID before updating this page's reference.
832
     *
833
     * @param SiteTree|int $item Either the parent object, or the parent ID
834
     */
835
    public function setParent($item)
836
    {
837
        if (is_object($item)) {
838
            if (!$item->exists()) {
839
                $item->write();
840
            }
841
            $this->setField("ParentID", $item->ID);
842
        } else {
843
            $this->setField("ParentID", $item);
844
        }
845
    }
846
847
    /**
848
     * Get the parent of this page.
849
     *
850
     * @return SiteTree Parent of this page
851
     */
852
    public function getParent()
853
    {
854
        if ($parentID = $this->getField("ParentID")) {
855
            return DataObject::get_by_id("SilverStripe\\CMS\\Model\\SiteTree", $parentID);
856
        }
857
        return null;
858
    }
859
860
    /**
861
     * Return a string of the form "parent - page" or "grandparent - parent - page" using page titles
862
     *
863
     * @param int $level The maximum amount of levels to traverse.
864
     * @param string $separator Seperating string
865
     * @return string The resulting string
866
     */
867
    public function NestedTitle($level = 2, $separator = " - ")
868
    {
869
        $item = $this;
870
        $parts = [];
871
        while ($item && $level > 0) {
872
            $parts[] = $item->Title;
873
            $item = $item->getParent();
874
            $level--;
875
        }
876
        return implode($separator, array_reverse($parts));
877
    }
878
879
    /**
880
     * This function should return true if the current user can execute this action. It can be overloaded to customise
881
     * the security model for an application.
882
     *
883
     * Slightly altered from parent behaviour in {@link DataObject->can()}:
884
     * - Checks for existence of a method named "can<$perm>()" on the object
885
     * - Calls decorators and only returns for FALSE "vetoes"
886
     * - Falls back to {@link Permission::check()}
887
     * - Does NOT check for many-many relations named "Can<$perm>"
888
     *
889
     * @uses DataObjectDecorator->can()
890
     *
891
     * @param string $perm The permission to be checked, such as 'View'
892
     * @param Member $member The member whose permissions need checking. Defaults to the currently logged in user.
893
     * @param array $context Context argument for canCreate()
894
     * @return bool True if the the member is allowed to do the given action
895
     */
896
    public function can($perm, $member = null, $context = array())
897
    {
898 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...
899
            $member = Member::currentUserID();
900
        }
901
902
        if ($member && Permission::checkMember($member, "ADMIN")) {
903
            return true;
904
        }
905
906
        if (is_string($perm) && method_exists($this, 'can' . ucfirst($perm))) {
907
            $method = 'can' . ucfirst($perm);
908
            return $this->$method($member);
909
        }
910
911
        $results = $this->extend('can', $member);
912
        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...
913
            if (!min($results)) {
914
                return false;
915
            }
916
        }
917
918
        return ($member && Permission::checkMember($member, $perm));
919
    }
920
921
    /**
922
     * This function should return true if the current user can add children to this page. It can be overloaded to
923
     * customise the security model for an application.
924
     *
925
     * Denies permission if any of the following conditions is true:
926
     * - alternateCanAddChildren() on a extension returns false
927
     * - canEdit() is not granted
928
     * - There are no classes defined in {@link $allowed_children}
929
     *
930
     * @uses SiteTreeExtension->canAddChildren()
931
     * @uses canEdit()
932
     * @uses $allowed_children
933
     *
934
     * @param Member|int $member
935
     * @return bool True if the current user can add children
936
     */
937
    public function canAddChildren($member = null)
938
    {
939
        // Disable adding children to archived pages
940
        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...
941
            return false;
942
        }
943
944 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...
945
            $member = Member::currentUserID();
946
        }
947
948
        // Standard mechanism for accepting permission changes from extensions
949
        $extended = $this->extendedCan('canAddChildren', $member);
950
        if ($extended !== null) {
951
            return $extended;
952
        }
953
954
        // Default permissions
955
        if ($member && Permission::checkMember($member, "ADMIN")) {
956
            return true;
957
        }
958
959
        return $this->canEdit($member) && $this->stat('allowed_children') != 'none';
960
    }
961
962
    /**
963
     * This function should return true if the current user can view this page. It can be overloaded to customise the
964
     * security model for an application.
965
     *
966
     * Denies permission if any of the following conditions is true:
967
     * - canView() on any extension returns false
968
     * - "CanViewType" directive is set to "Inherit" and any parent page return false for canView()
969
     * - "CanViewType" directive is set to "LoggedInUsers" and no user is logged in
970
     * - "CanViewType" directive is set to "OnlyTheseUsers" and user is not in the given groups
971
     *
972
     * @uses DataExtension->canView()
973
     * @uses ViewerGroups()
974
     *
975
     * @param Member|int $member
976
     * @return bool True if the current user can view this page
977
     */
978
    public function canView($member = null)
979
    {
980 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...
981
            $member = Member::currentUserID();
982
        }
983
984
        // Standard mechanism for accepting permission changes from extensions
985
        $extended = $this->extendedCan('canView', $member);
986
        if ($extended !== null) {
987
            return $extended;
988
        }
989
990
        // admin override
991
        if ($member && Permission::checkMember($member, array("ADMIN", "SITETREE_VIEW_ALL"))) {
992
            return true;
993
        }
994
995
        // Orphaned pages (in the current stage) are unavailable, except for admins via the CMS
996
        if ($this->isOrphaned()) {
997
            return false;
998
        }
999
1000
        // check for empty spec
1001
        if (!$this->CanViewType || $this->CanViewType == 'Anyone') {
1002
            return true;
1003
        }
1004
1005
        // check for inherit
1006
        if ($this->CanViewType == 'Inherit') {
1007
            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...
1008
                return $this->Parent()->canView($member);
1009
            } else {
1010
                return $this->getSiteConfig()->canViewPages($member);
1011
            }
1012
        }
1013
1014
        // check for any logged-in users
1015
        if ($this->CanViewType == 'LoggedInUsers' && $member) {
1016
            return true;
1017
        }
1018
1019
        // check for specific groups
1020
        if ($member && is_numeric($member)) {
1021
            $member = DataObject::get_by_id('SilverStripe\\Security\\Member', $member);
1022
        }
1023
        if ($this->CanViewType == 'OnlyTheseUsers'
1024
            && $member
1025
            && $member->inGroups($this->ViewerGroups())
1026
        ) {
1027
            return true;
1028
        }
1029
1030
        return false;
1031
    }
1032
1033
    /**
1034
     * Check if this page can be published
1035
     *
1036
     * @param Member $member
1037
     * @return bool
1038
     */
1039
    public function canPublish($member = null)
1040
    {
1041
        if (!$member) {
1042
            $member = Member::currentUser();
1043
        }
1044
1045
        // Check extension
1046
        $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...
1047
        if ($extended !== null) {
1048
            return $extended;
1049
        }
1050
1051
        if (Permission::checkMember($member, "ADMIN")) {
1052
            return true;
1053
        }
1054
1055
        // Default to relying on edit permission
1056
        return $this->canEdit($member);
1057
    }
1058
1059
    /**
1060
     * This function should return true if the current user can delete this page. It can be overloaded to customise the
1061
     * security model for an application.
1062
     *
1063
     * Denies permission if any of the following conditions is true:
1064
     * - canDelete() returns false on any extension
1065
     * - canEdit() returns false
1066
     * - any descendant page returns false for canDelete()
1067
     *
1068
     * @uses canDelete()
1069
     * @uses SiteTreeExtension->canDelete()
1070
     * @uses canEdit()
1071
     *
1072
     * @param Member $member
1073
     * @return bool True if the current user can delete this page
1074
     */
1075
    public function canDelete($member = null)
1076
    {
1077 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...
1078
            $memberID = $member->ID;
1079
        } elseif (is_numeric($member)) {
1080
            $memberID = $member;
1081
        } else {
1082
            $memberID = Member::currentUserID();
1083
        }
1084
1085
        // Standard mechanism for accepting permission changes from extensions
1086
        $extended = $this->extendedCan('canDelete', $memberID);
1087
        if ($extended !== null) {
1088
            return $extended;
1089
        }
1090
1091
        // Default permission check
1092
        if ($memberID && Permission::checkMember($memberID, array("ADMIN", "SITETREE_EDIT_ALL"))) {
1093
            return true;
1094
        }
1095
1096
        // Regular canEdit logic is handled by can_edit_multiple
1097
        $results = self::can_delete_multiple(array($this->ID), $memberID);
1098
1099
        // If this page no longer exists in stage/live results won't contain the page.
1100
        // Fail-over to false
1101
        return isset($results[$this->ID]) ? $results[$this->ID] : false;
1102
    }
1103
1104
    /**
1105
     * This function should return true if the current user can create new pages of this class, regardless of class. It
1106
     * can be overloaded to customise the security model for an application.
1107
     *
1108
     * By default, permission to create at the root level is based on the SiteConfig configuration, and permission to
1109
     * create beneath a parent is based on the ability to edit that parent page.
1110
     *
1111
     * Use {@link canAddChildren()} to control behaviour of creating children under this page.
1112
     *
1113
     * @uses $can_create
1114
     * @uses DataExtension->canCreate()
1115
     *
1116
     * @param Member $member
1117
     * @param array $context Optional array which may contain array('Parent' => $parentObj)
1118
     *                       If a parent page is known, it will be checked for validity.
1119
     *                       If omitted, it will be assumed this is to be created as a top level page.
1120
     * @return bool True if the current user can create pages on this class.
1121
     */
1122
    public function canCreate($member = null, $context = array())
1123
    {
1124 View Code Duplication
        if (!$member || !(is_a($member, 'SilverStripe\\Security\\Member')) || is_numeric($member)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
1125
            $member = Member::currentUserID();
1126
        }
1127
1128
        // Check parent (custom canCreate option for SiteTree)
1129
        // Block children not allowed for this parent type
1130
        $parent = isset($context['Parent']) ? $context['Parent'] : null;
1131
        if ($parent && !in_array(static::class, $parent->allowedChildren())) {
1132
            return false;
1133
        }
1134
1135
        // Standard mechanism for accepting permission changes from extensions
1136
        $extended = $this->extendedCan(__FUNCTION__, $member, $context);
1137
        if ($extended !== null) {
1138
            return $extended;
1139
        }
1140
1141
        // Check permission
1142
        if ($member && Permission::checkMember($member, "ADMIN")) {
1143
            return true;
1144
        }
1145
1146
        // Fall over to inherited permissions
1147
        if ($parent) {
1148
            return $parent->canAddChildren($member);
1149
        } else {
1150
            // This doesn't necessarily mean we are creating a root page, but that
1151
            // we don't know if there is a parent, so default to this permission
1152
            return SiteConfig::current_site_config()->canCreateTopLevel($member);
1153
        }
1154
    }
1155
1156
    /**
1157
     * This function should return true if the current user can edit this page. It can be overloaded to customise the
1158
     * security model for an application.
1159
     *
1160
     * Denies permission if any of the following conditions is true:
1161
     * - canEdit() on any extension returns false
1162
     * - canView() return false
1163
     * - "CanEditType" directive is set to "Inherit" and any parent page return false for canEdit()
1164
     * - "CanEditType" directive is set to "LoggedInUsers" and no user is logged in or doesn't have the
1165
     *   CMS_Access_CMSMAIN permission code
1166
     * - "CanEditType" directive is set to "OnlyTheseUsers" and user is not in the given groups
1167
     *
1168
     * @uses canView()
1169
     * @uses EditorGroups()
1170
     * @uses DataExtension->canEdit()
1171
     *
1172
     * @param Member $member Set to false if you want to explicitly test permissions without a valid user (useful for
1173
     *                       unit tests)
1174
     * @return bool True if the current user can edit this page
1175
     */
1176
    public function canEdit($member = null)
1177
    {
1178 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...
1179
            $memberID = $member->ID;
1180
        } elseif (is_numeric($member)) {
1181
            $memberID = $member;
1182
        } else {
1183
            $memberID = Member::currentUserID();
1184
        }
1185
1186
        // Standard mechanism for accepting permission changes from extensions
1187
        $extended = $this->extendedCan('canEdit', $memberID);
1188
        if ($extended !== null) {
1189
            return $extended;
1190
        }
1191
1192
        // Default permissions
1193
        if ($memberID && Permission::checkMember($memberID, array("ADMIN", "SITETREE_EDIT_ALL"))) {
1194
            return true;
1195
        }
1196
1197
        if ($this->ID) {
1198
            // Regular canEdit logic is handled by can_edit_multiple
1199
            $results = self::can_edit_multiple(array($this->ID), $memberID);
1200
1201
            // If this page no longer exists in stage/live results won't contain the page.
1202
            // Fail-over to false
1203
            return isset($results[$this->ID]) ? $results[$this->ID] : false;
1204
1205
        // Default for unsaved pages
1206
        } else {
1207
            return $this->getSiteConfig()->canEditPages($member);
1208
        }
1209
    }
1210
1211
    /**
1212
     * Stub method to get the site config, unless the current class can provide an alternate.
1213
     *
1214
     * @return SiteConfig
1215
     */
1216
    public function getSiteConfig()
1217
    {
1218
        $configs = $this->invokeWithExtensions('alternateSiteConfig');
1219
        foreach (array_filter($configs) as $config) {
1220
            return $config;
1221
        }
1222
1223
        return SiteConfig::current_site_config();
1224
    }
1225
1226
    /**
1227
     * Pre-populate the cache of canEdit, canView, canDelete, canPublish permissions. This method will use the static
1228
     * can_(perm)_multiple method for efficiency.
1229
     *
1230
     * @param string          $permission    The permission: edit, view, publish, approve, etc.
1231
     * @param array           $ids           An array of page IDs
1232
     * @param callable|string $batchCallback The function/static method to call to calculate permissions.  Defaults
1233
     *                                       to 'SiteTree::can_(permission)_multiple'
1234
     */
1235
    public static function prepopulate_permission_cache($permission = 'CanEditType', $ids, $batchCallback = null)
1236
    {
1237
        if (!$batchCallback) {
1238
            $batchCallback = self::class . "::can_{$permission}_multiple";
1239
        }
1240
1241
        if (is_callable($batchCallback)) {
1242
            call_user_func($batchCallback, $ids, Member::currentUserID(), false);
1243
        } else {
1244
            user_error("SiteTree::prepopulate_permission_cache can't calculate '$permission' "
1245
                . "with callback '$batchCallback'", E_USER_WARNING);
1246
        }
1247
    }
1248
1249
    /**
1250
     * This method is NOT a full replacement for the individual can*() methods, e.g. {@link canEdit()}. Rather than
1251
     * checking (potentially slow) PHP logic, it relies on the database group associations, e.g. the "CanEditType" field
1252
     * plus the "SiteTree_EditorGroups" many-many table. By batch checking multiple records, we can combine the queries
1253
     * efficiently.
1254
     *
1255
     * Caches based on $typeField data. To invalidate the cache, use {@link SiteTree::reset()} or set the $useCached
1256
     * property to FALSE.
1257
     *
1258
     * @param array  $ids              Of {@link SiteTree} IDs
1259
     * @param int    $memberID         Member ID
1260
     * @param string $typeField        A property on the data record, e.g. "CanEditType".
1261
     * @param string $groupJoinTable   A many-many table name on this record, e.g. "SiteTree_EditorGroups"
1262
     * @param string $siteConfigMethod Method to call on {@link SiteConfig} for toplevel items, e.g. "canEdit"
1263
     * @param string $globalPermission If the member doesn't have this permission code, don't bother iterating deeper
1264
     * @param bool   $useCached
1265
     * @return array An map of {@link SiteTree} ID keys to boolean values
1266
     */
1267
    public static function batch_permission_check(
1268
        $ids,
1269
        $memberID,
1270
        $typeField,
1271
        $groupJoinTable,
1272
        $siteConfigMethod,
1273
        $globalPermission = null,
1274
        $useCached = true
1275
    ) {
1276
        if ($globalPermission === null) {
1277
            $globalPermission = array('CMS_ACCESS_LeftAndMain', 'CMS_ACCESS_CMSMain');
1278
        }
1279
1280
        // Sanitise the IDs
1281
        $ids = array_filter($ids, 'is_numeric');
1282
1283
        // This is the name used on the permission cache
1284
        // converts something like 'CanEditType' to 'edit'.
1285
        $cacheKey = strtolower(substr($typeField, 3, -4)) . "-$memberID";
1286
1287
        // Default result: nothing editable
1288
        $result = array_fill_keys($ids, false);
1289
        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...
1290
            // Look in the cache for values
1291
            if ($useCached && isset(self::$cache_permissions[$cacheKey])) {
1292
                $cachedValues = array_intersect_key(self::$cache_permissions[$cacheKey], $result);
1293
1294
                // If we can't find everything in the cache, then look up the remainder separately
1295
                $uncachedValues = array_diff_key($result, self::$cache_permissions[$cacheKey]);
1296
                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...
1297
                    $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 1277 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...
1298
                }
1299
                return $cachedValues;
1300
            }
1301
1302
            // If a member doesn't have a certain permission then they can't edit anything
1303
            if (!$memberID || ($globalPermission && !Permission::checkMember($memberID, $globalPermission))) {
1304
                return $result;
1305
            }
1306
1307
            // Placeholder for parameterised ID list
1308
            $idPlaceholders = DB::placeholders($ids);
1309
1310
            // If page can't be viewed, don't grant edit permissions to do - implement can_view_multiple(), so this can
1311
            // be enabled
1312
            //$ids = array_keys(array_filter(self::can_view_multiple($ids, $memberID)));
1313
1314
            // Get the groups that the given member belongs to
1315
            /** @var Member $member */
1316
            $member = DataObject::get_by_id('SilverStripe\\Security\\Member', $memberID);
1317
            $groupIDs = $member->Groups()->column("ID");
1318
            $SQL_groupList = implode(", ", $groupIDs);
1319
            if (!$SQL_groupList) {
1320
                $SQL_groupList = '0';
1321
            }
1322
1323
            $combinedStageResult = array();
1324
1325
            foreach (array(Versioned::DRAFT, Versioned::LIVE) as $stage) {
1326
                // Start by filling the array with the pages that actually exist
1327
                /** @skipUpgrade */
1328
                $table = ($stage=='Stage') ? "SiteTree" : "SiteTree_$stage";
1329
1330
                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...
1331
                    $idQuery = "SELECT \"ID\" FROM \"$table\" WHERE \"ID\" IN ($idPlaceholders)";
1332
                    $stageIds = DB::prepared_query($idQuery, $ids)->column();
1333
                } else {
1334
                    $stageIds = array();
1335
                }
1336
                $result = array_fill_keys($stageIds, false);
1337
1338
                // Get the uninherited permissions
1339
                $uninheritedPermissions = Versioned::get_by_stage("SilverStripe\\CMS\\Model\\SiteTree", $stage)
1340
                    ->where(array(
1341
                        "(\"$typeField\" = 'LoggedInUsers' OR
1342
						(\"$typeField\" = 'OnlyTheseUsers' AND \"$groupJoinTable\".\"SiteTreeID\" IS NOT NULL))
1343
						AND \"SiteTree\".\"ID\" IN ($idPlaceholders)"
1344
                        => $ids
1345
                    ))
1346
                    ->leftJoin($groupJoinTable, "\"$groupJoinTable\".\"SiteTreeID\" = \"SiteTree\".\"ID\" AND \"$groupJoinTable\".\"GroupID\" IN ($SQL_groupList)");
1347
1348
                if ($uninheritedPermissions) {
1349
                    // Set all the relevant items in $result to true
1350
                    $result = array_fill_keys($uninheritedPermissions->column('ID'), true) + $result;
1351
                }
1352
1353
                // Get permissions that are inherited
1354
                $potentiallyInherited = Versioned::get_by_stage(
1355
                    "SilverStripe\\CMS\\Model\\SiteTree",
1356
                    $stage,
1357
                    array("\"$typeField\" = 'Inherit' AND \"SiteTree\".\"ID\" IN ($idPlaceholders)" => $ids)
0 ignored issues
show
Documentation introduced by
array("\"{$typeField}\" ...laceholders})" => $ids) is of type array<string|integer,array>, but the function expects a string.

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1358
                );
1359
1360
                if ($potentiallyInherited) {
1361
                    // Group $potentiallyInherited by ParentID; we'll look at the permission of all those parents and
1362
                    // then see which ones the user has permission on
1363
                    $groupedByParent = array();
1364
                    foreach ($potentiallyInherited as $item) {
1365
                        /** @var SiteTree $item */
1366
                        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...
1367
                            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...
1368
                                $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...
1369
                            }
1370
                            $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...
1371
                        } else {
1372
                            // Might return different site config based on record context, e.g. when subsites module
1373
                            // is used
1374
                            $siteConfig = $item->getSiteConfig();
1375
                            $result[$item->ID] = $siteConfig->{$siteConfigMethod}($memberID);
1376
                        }
1377
                    }
1378
1379
                    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...
1380
                        $actuallyInherited = self::batch_permission_check(array_keys($groupedByParent), $memberID, $typeField, $groupJoinTable, $siteConfigMethod);
1381
                        if ($actuallyInherited) {
1382
                            $parentIDs = array_keys(array_filter($actuallyInherited));
1383
                            foreach ($parentIDs as $parentID) {
1384
                                // Set all the relevant items in $result to true
1385
                                $result = array_fill_keys($groupedByParent[$parentID], true) + $result;
1386
                            }
1387
                        }
1388
                    }
1389
                }
1390
1391
                $combinedStageResult = $combinedStageResult + $result;
1392
            }
1393
        }
1394
1395
        if (isset($combinedStageResult)) {
1396
            // Cache the results
1397
            if (empty(self::$cache_permissions[$cacheKey])) {
1398
                self::$cache_permissions[$cacheKey] = array();
1399
            }
1400
            self::$cache_permissions[$cacheKey] = $combinedStageResult + self::$cache_permissions[$cacheKey];
1401
            return $combinedStageResult;
1402
        } else {
1403
            return array();
1404
        }
1405
    }
1406
1407
    /**
1408
     * Get the 'can edit' information for a number of SiteTree pages.
1409
     *
1410
     * @param array $ids       An array of IDs of the SiteTree pages to look up
1411
     * @param int   $memberID  ID of member
1412
     * @param bool  $useCached Return values from the permission cache if they exist
1413
     * @return array A map where the IDs are keys and the values are booleans stating whether the given page can be
1414
     *                         edited
1415
     */
1416
    public static function can_edit_multiple($ids, $memberID, $useCached = true)
1417
    {
1418
        return self::batch_permission_check($ids, $memberID, 'CanEditType', 'SiteTree_EditorGroups', 'canEditPages', null, $useCached);
1419
    }
1420
1421
    /**
1422
     * Get the 'can edit' information for a number of SiteTree pages.
1423
     *
1424
     * @param array $ids       An array of IDs of the SiteTree pages to look up
1425
     * @param int   $memberID  ID of member
1426
     * @param bool  $useCached Return values from the permission cache if they exist
1427
     * @return array
1428
     */
1429
    public static function can_delete_multiple($ids, $memberID, $useCached = true)
1430
    {
1431
        $deletable = array();
1432
        $result = array_fill_keys($ids, false);
1433
        $cacheKey = "delete-$memberID";
1434
1435
        // Look in the cache for values
1436
        if ($useCached && isset(self::$cache_permissions[$cacheKey])) {
1437
            $cachedValues = array_intersect_key(self::$cache_permissions[$cacheKey], $result);
1438
1439
            // If we can't find everything in the cache, then look up the remainder separately
1440
            $uncachedValues = array_diff_key($result, self::$cache_permissions[$cacheKey]);
1441
            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...
1442
                $cachedValues = self::can_delete_multiple(array_keys($uncachedValues), $memberID, false)
1443
                    + $cachedValues;
1444
            }
1445
            return $cachedValues;
1446
        }
1447
1448
        // You can only delete pages that you can edit
1449
        $editableIDs = array_keys(array_filter(self::can_edit_multiple($ids, $memberID)));
1450
        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...
1451
            // You can only delete pages whose children you can delete
1452
            $editablePlaceholders = DB::placeholders($editableIDs);
1453
            $childRecords = SiteTree::get()->where(array(
1454
                "\"SiteTree\".\"ParentID\" IN ($editablePlaceholders)" => $editableIDs
1455
            ));
1456
            if ($childRecords) {
1457
                $children = $childRecords->map("ID", "ParentID");
1458
1459
                // Find out the children that can be deleted
1460
                $deletableChildren = self::can_delete_multiple($children->keys(), $memberID);
1461
1462
                // Get a list of all the parents that have no undeletable children
1463
                $deletableParents = array_fill_keys($editableIDs, true);
1464
                foreach ($deletableChildren as $id => $canDelete) {
1465
                    if (!$canDelete) {
1466
                        unset($deletableParents[$children[$id]]);
1467
                    }
1468
                }
1469
1470
                // Use that to filter the list of deletable parents that have children
1471
                $deletableParents = array_keys($deletableParents);
1472
1473
                // Also get the $ids that don't have children
1474
                $parents = array_unique($children->values());
1475
                $deletableLeafNodes = array_diff($editableIDs, $parents);
1476
1477
                // Combine the two
1478
                $deletable = array_merge($deletableParents, $deletableLeafNodes);
1479
            } else {
1480
                $deletable = $editableIDs;
1481
            }
1482
        }
1483
1484
        // Convert the array of deletable IDs into a map of the original IDs with true/false as the value
1485
        return array_fill_keys($deletable, true) + array_fill_keys($ids, false);
1486
    }
1487
1488
    /**
1489
     * Collate selected descendants of this page.
1490
     *
1491
     * {@link $condition} will be evaluated on each descendant, and if it is succeeds, that item will be added to the
1492
     * $collator array.
1493
     *
1494
     * @param string $condition The PHP condition to be evaluated. The page will be called $item
1495
     * @param array  $collator  An array, passed by reference, to collect all of the matching descendants.
1496
     * @return bool
1497
     */
1498
    public function collateDescendants($condition, &$collator)
1499
    {
1500
        $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...
1501
        if ($children) {
1502
            foreach ($children as $item) {
1503
                if (eval("return $condition;")) {
1504
                    $collator[] = $item;
1505
                }
1506
                /** @var SiteTree $item */
1507
                $item->collateDescendants($condition, $collator);
1508
            }
1509
            return true;
1510
        }
1511
        return false;
1512
    }
1513
1514
    /**
1515
     * Return the title, description, keywords and language metatags.
1516
     *
1517
     * @todo Move <title> tag in separate getter for easier customization and more obvious usage
1518
     *
1519
     * @param bool $includeTitle Show default <title>-tag, set to false for custom templating
1520
     * @return string The XHTML metatags
1521
     */
1522
    public function MetaTags($includeTitle = true)
1523
    {
1524
        $tags = array();
1525
        if ($includeTitle && strtolower($includeTitle) != 'false') {
1526
            $tags[] = FormField::create_tag('title', array(), $this->obj('Title')->forTemplate());
1527
        }
1528
1529
        $generator = trim(Config::inst()->get(self::class, 'meta_generator'));
1530
        if (!empty($generator)) {
1531
            $tags[] = FormField::create_tag('meta', array(
1532
                'name' => 'generator',
1533
                'content' => $generator,
1534
            ));
1535
        }
1536
1537
        $charset = Config::inst()->get('SilverStripe\\Control\\ContentNegotiator', 'encoding');
1538
        $tags[] = FormField::create_tag('meta', array(
1539
            'http-equiv' => 'Content-Type',
1540
            'content' => 'text/html; charset=' . $charset,
1541
        ));
1542
        if ($this->MetaDescription) {
1543
            $tags[] = FormField::create_tag('meta', array(
1544
                'name' => 'description',
1545
                'content' => $this->MetaDescription,
1546
            ));
1547
        }
1548
1549
        if (Permission::check('CMS_ACCESS_CMSMain')
1550
            && !$this instanceof ErrorPage
1551
            && $this->ID > 0
1552
        ) {
1553
            $tags[] = FormField::create_tag('meta', array(
1554
                'name' => 'x-page-id',
1555
                'content' => $this->obj('ID')->forTemplate(),
1556
            ));
1557
            $tags[] = FormField::create_tag('meta', array(
1558
                'name' => 'x-cms-edit-link',
1559
                'content' => $this->obj('CMSEditLink')->forTemplate(),
1560
            ));
1561
        }
1562
1563
        $tags = implode("\n", $tags);
1564
        if ($this->ExtraMeta) {
1565
            $tags .= $this->obj('ExtraMeta')->forTemplate();
1566
        }
1567
1568
        $this->extend('MetaTags', $tags);
1569
1570
        return $tags;
1571
    }
1572
1573
    /**
1574
     * Returns the object that contains the content that a user would associate with this page.
1575
     *
1576
     * Ordinarily, this is just the page itself, but for example on RedirectorPages or VirtualPages ContentSource() will
1577
     * return the page that is linked to.
1578
     *
1579
     * @return $this
1580
     */
1581
    public function ContentSource()
1582
    {
1583
        return $this;
1584
    }
1585
1586
    /**
1587
     * Add default records to database.
1588
     *
1589
     * This function is called whenever the database is built, after the database tables have all been created. Overload
1590
     * this to add default records when the database is built, but make sure you call parent::requireDefaultRecords().
1591
     */
1592
    public function requireDefaultRecords()
1593
    {
1594
        parent::requireDefaultRecords();
1595
1596
        // default pages
1597
        if (static::class == self::class && $this->config()->create_default_pages) {
1598
            if (!SiteTree::get_by_link(RootURLController::config()->default_homepage_link)) {
1599
                $homepage = new Page();
1600
                $homepage->Title = _t('SiteTree.DEFAULTHOMETITLE', 'Home');
1601
                $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>');
1602
                $homepage->URLSegment = RootURLController::config()->default_homepage_link;
1603
                $homepage->Sort = 1;
1604
                $homepage->write();
1605
                $homepage->copyVersionToStage(Versioned::DRAFT, Versioned::LIVE);
1606
                $homepage->flushCache();
1607
                DB::alteration_message('Home page created', 'created');
1608
            }
1609
1610
            if (DB::query("SELECT COUNT(*) FROM \"SiteTree\"")->value() == 1) {
1611
                $aboutus = new Page();
1612
                $aboutus->Title = _t('SiteTree.DEFAULTABOUTTITLE', 'About Us');
1613
                $aboutus->Content = _t(
1614
                    'SiteTree.DEFAULTABOUTCONTENT',
1615
                    '<p>You can fill this page out with your own content, or delete it and create your own pages.</p>'
1616
                );
1617
                $aboutus->Sort = 2;
1618
                $aboutus->write();
1619
                $aboutus->copyVersionToStage(Versioned::DRAFT, Versioned::LIVE);
1620
                $aboutus->flushCache();
1621
                DB::alteration_message('About Us page created', 'created');
1622
1623
                $contactus = new Page();
1624
                $contactus->Title = _t('SiteTree.DEFAULTCONTACTTITLE', 'Contact Us');
1625
                $contactus->Content = _t(
1626
                    'SiteTree.DEFAULTCONTACTCONTENT',
1627
                    '<p>You can fill this page out with your own content, or delete it and create your own pages.</p>'
1628
                );
1629
                $contactus->Sort = 3;
1630
                $contactus->write();
1631
                $contactus->copyVersionToStage(Versioned::DRAFT, Versioned::LIVE);
1632
                $contactus->flushCache();
1633
                DB::alteration_message('Contact Us page created', 'created');
1634
            }
1635
        }
1636
    }
1637
1638
    protected function onBeforeWrite()
1639
    {
1640
        parent::onBeforeWrite();
1641
1642
        // If Sort hasn't been set, make this page come after it's siblings
1643
        if (!$this->Sort) {
1644
            $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...
1645
            $this->Sort = DB::prepared_query(
1646
                "SELECT MAX(\"Sort\") + 1 FROM \"SiteTree\" WHERE \"ParentID\" = ?",
1647
                array($parentID)
1648
            )->value();
1649
        }
1650
1651
        // If there is no URLSegment set, generate one from Title
1652
        $defaultSegment = $this->generateURLSegment(_t(
1653
            'CMSMain.NEWPAGE',
1654
            array('pagetype' => $this->i18n_singular_name())
0 ignored issues
show
Documentation introduced by
array('pagetype' => $this->i18n_singular_name()) is of type array<string,string,{"pagetype":"string"}>, but the function expects a string.

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1655
        ));
1656
        if ((!$this->URLSegment || $this->URLSegment == $defaultSegment) && $this->Title) {
1657
            $this->URLSegment = $this->generateURLSegment($this->Title);
1658
        } elseif ($this->isChanged('URLSegment', 2)) {
1659
            // Do a strict check on change level, to avoid double encoding caused by
1660
            // bogus changes through forceChange()
1661
            $filter = URLSegmentFilter::create();
1662
            $this->URLSegment = $filter->filter($this->URLSegment);
1663
            // If after sanitising there is no URLSegment, give it a reasonable default
1664
            if (!$this->URLSegment) {
1665
                $this->URLSegment = "page-$this->ID";
1666
            }
1667
        }
1668
1669
        // Ensure that this object has a non-conflicting URLSegment value.
1670
        $count = 2;
1671
        while (!$this->validURLSegment()) {
1672
            $this->URLSegment = preg_replace('/-[0-9]+$/', null, $this->URLSegment) . '-' . $count;
1673
            $count++;
1674
        }
1675
1676
        $this->syncLinkTracking();
1677
1678
        // Check to see if we've only altered fields that shouldn't affect versioning
1679
        $fieldsIgnoredByVersioning = array('HasBrokenLink', 'Status', 'HasBrokenFile', 'ToDo', 'VersionID', 'SaveCount');
1680
        $changedFields = array_keys($this->getChangedFields(true, 2));
1681
1682
        // This more rigorous check is inline with the test that write() does to decide whether or not to write to the
1683
        // DB. We use that to avoid cluttering the system with a migrateVersion() call that doesn't get used
1684
        $oneChangedFields = array_keys($this->getChangedFields(true, 1));
1685
1686
        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...
1687
            // This will have the affect of preserving the versioning
1688
            $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...
1689
        }
1690
    }
1691
1692
    /**
1693
     * Trigger synchronisation of link tracking
1694
     *
1695
     * {@see SiteTreeLinkTracking::augmentSyncLinkTracking}
1696
     */
1697
    public function syncLinkTracking()
1698
    {
1699
        $this->extend('augmentSyncLinkTracking');
1700
    }
1701
1702
    public function onBeforeDelete()
1703
    {
1704
        parent::onBeforeDelete();
1705
1706
        // If deleting this page, delete all its children.
1707
        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...
1708
            foreach ($children as $child) {
1709
                /** @var SiteTree $child */
1710
                $child->delete();
1711
            }
1712
        }
1713
    }
1714
1715
    public function onAfterDelete()
1716
    {
1717
        // Need to flush cache to avoid outdated versionnumber references
1718
        $this->flushCache();
1719
1720
        // Need to mark pages depending to this one as broken
1721
        $dependentPages = $this->DependentPages();
1722
        if ($dependentPages) {
1723
            foreach ($dependentPages as $page) {
1724
                        // $page->write() calls syncLinkTracking, which does all the hard work for us.
1725
                $page->write();
1726
            }
1727
        }
1728
1729
        parent::onAfterDelete();
1730
    }
1731
1732
    public function flushCache($persistent = true)
1733
    {
1734
        parent::flushCache($persistent);
1735
        $this->_cache_statusFlags = null;
1736
    }
1737
1738
    public function validate()
1739
    {
1740
        $result = parent::validate();
1741
1742
        // Allowed children validation
1743
        $parent = $this->getParent();
1744
        if ($parent && $parent->exists()) {
1745
            // No need to check for subclasses or instanceof, as allowedChildren() already
1746
            // deconstructs any inheritance trees already.
1747
            $allowed = $parent->allowedChildren();
1748
            $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...
1749
                ? $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...
1750
                : $this;
1751
            if (!in_array($subject->ClassName, $allowed)) {
1752
                $result->error(
1753
                    _t(
1754
                        'SiteTree.PageTypeNotAllowed',
1755
                        'Page type "{type}" not allowed as child of this parent page',
1756
                        array('type' => $subject->i18n_singular_name())
0 ignored issues
show
Documentation introduced by
array('type' => $subject->i18n_singular_name()) is of type array<string,?,{"type":"?"}>, but the function expects a string.

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1757
                    ),
1758
                    'ALLOWED_CHILDREN'
1759
                );
1760
            }
1761
        }
1762
1763
        // "Can be root" validation
1764
        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...
1765
            $result->error(
1766
                _t(
1767
                    'SiteTree.PageTypNotAllowedOnRoot',
1768
                    'Page type "{type}" is not allowed on the root level',
1769
                    array('type' => $this->i18n_singular_name())
0 ignored issues
show
Documentation introduced by
array('type' => $this->i18n_singular_name()) is of type array<string,string,{"type":"string"}>, but the function expects a string.

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1770
                ),
1771
                'CAN_BE_ROOT'
1772
            );
1773
        }
1774
1775
        return $result;
1776
    }
1777
1778
    /**
1779
     * Returns true if this object has a URLSegment value that does not conflict with any other objects. This method
1780
     * checks for:
1781
     *  - A page with the same URLSegment that has a conflict
1782
     *  - Conflicts with actions on the parent page
1783
     *  - A conflict caused by a root page having the same URLSegment as a class name
1784
     *
1785
     * @return bool
1786
     */
1787
    public function validURLSegment()
1788
    {
1789
        if (self::config()->nested_urls && $parent = $this->Parent()) {
1790
            if ($controller = ModelAsController::controller_for($parent)) {
1791
                if ($controller instanceof Controller && $controller->hasAction($this->URLSegment)) {
1792
                    return false;
1793
                }
1794
            }
1795
        }
1796
1797
        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...
1798
            if (class_exists($this->URLSegment) && is_subclass_of($this->URLSegment, 'SilverStripe\\Control\\RequestHandler')) {
1799
                return false;
1800
            }
1801
        }
1802
1803
        // Filters by url, id, and parent
1804
        $filter = array('"SiteTree"."URLSegment"' => $this->URLSegment);
1805
        if ($this->ID) {
1806
            $filter['"SiteTree"."ID" <> ?'] = $this->ID;
1807
        }
1808
        if (self::config()->nested_urls) {
1809
            $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...
1810
        }
1811
1812
        $votes = array_filter(
1813
            (array)$this->extend('augmentValidURLSegment'),
1814
            function ($v) {
1815
                return !is_null($v);
1816
            }
1817
        );
1818
        if ($votes) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $votes 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...
1819
            return min($votes);
1820
        }
1821
1822
        // Check existence
1823
        $existingPage = DataObject::get_one(self::class, $filter);
1824
        if ($existingPage) {
1825
            return false;
1826
        }
1827
1828
        return !($existingPage);
1829
    }
1830
1831
    /**
1832
     * Generate a URL segment based on the title provided.
1833
     *
1834
     * If {@link Extension}s wish to alter URL segment generation, they can do so by defining
1835
     * updateURLSegment(&$url, $title).  $url will be passed by reference and should be modified. $title will contain
1836
     * the title that was originally used as the source of this generated URL. This lets extensions either start from
1837
     * scratch, or incrementally modify the generated URL.
1838
     *
1839
     * @param string $title Page title
1840
     * @return string Generated url segment
1841
     */
1842
    public function generateURLSegment($title)
1843
    {
1844
        $filter = URLSegmentFilter::create();
1845
        $t = $filter->filter($title);
1846
1847
        // Fallback to generic page name if path is empty (= no valid, convertable characters)
1848
        if (!$t || $t == '-' || $t == '-1') {
1849
            $t = "page-$this->ID";
1850
        }
1851
1852
        // Hook for extensions
1853
        $this->extend('updateURLSegment', $t, $title);
1854
1855
        return $t;
1856
    }
1857
1858
    /**
1859
     * Gets the URL segment for the latest draft version of this page.
1860
     *
1861
     * @return string
1862
     */
1863
    public function getStageURLSegment()
1864
    {
1865
        $stageRecord = Versioned::get_one_by_stage(self::class, Versioned::DRAFT, array(
0 ignored issues
show
Documentation introduced by
array('"SiteTree"."ID"' => $this->ID) is of type array<string,integer|str...D\"":"integer|string"}>, but the function expects a string.

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1866
            '"SiteTree"."ID"' => $this->ID
1867
        ));
1868
        return ($stageRecord) ? $stageRecord->URLSegment : null;
1869
    }
1870
1871
    /**
1872
     * Gets the URL segment for the currently published version of this page.
1873
     *
1874
     * @return string
1875
     */
1876
    public function getLiveURLSegment()
1877
    {
1878
        $liveRecord = Versioned::get_one_by_stage(self::class, Versioned::LIVE, array(
0 ignored issues
show
Documentation introduced by
array('"SiteTree"."ID"' => $this->ID) is of type array<string,integer|str...D\"":"integer|string"}>, but the function expects a string.

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

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

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
2007
                );
2008
            }
2009
        }
2010
2011
        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...
2012
            $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...
2013
        }
2014
2015
        $dependentNote = '';
2016
        $dependentTable = new LiteralField('DependentNote', '<p></p>');
2017
2018
        // Create a table for showing pages linked to this one
2019
        $dependentPages = $this->DependentPages();
2020
        $dependentPagesCount = $dependentPages->count();
2021
        if ($dependentPagesCount) {
2022
            $dependentColumns = array(
2023
                'Title' => $this->fieldLabel('Title'),
2024
                'AbsoluteLink' => _t('SiteTree.DependtPageColumnURL', 'URL'),
2025
                'DependentLinkType' => _t('SiteTree.DependtPageColumnLinkType', 'Link type'),
2026
            );
2027
            if (class_exists('Subsite')) {
2028
                $dependentColumns['Subsite.Title'] = singleton('Subsite')->i18n_singular_name();
2029
            }
2030
2031
            $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>');
2032
            $dependentTable = GridField::create(
2033
                'DependentPages',
2034
                false,
2035
                $dependentPages
2036
            );
2037
            /** @var GridFieldDataColumns $dataColumns */
2038
            $dataColumns = $dependentTable->getConfig()->getComponentByType('SilverStripe\\Forms\\GridField\\GridFieldDataColumns');
2039
            $dataColumns
2040
                ->setDisplayFields($dependentColumns)
2041
                ->setFieldFormatting(array(
2042
                    'Title' => function ($value, &$item) {
2043
                        return sprintf(
2044
                            '<a href="admin/pages/edit/show/%d">%s</a>',
2045
                            (int)$item->ID,
2046
                            Convert::raw2xml($item->Title)
2047
                        );
2048
                    },
2049
                    '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...
2050
                        return sprintf(
2051
                            '<a href="%s" target="_blank">%s</a>',
2052
                            Convert::raw2xml($value),
2053
                            Convert::raw2xml($value)
2054
                        );
2055
                    }
2056
                ));
2057
        }
2058
2059
        $baseLink = Controller::join_links(
2060
            Director::absoluteBaseURL(),
2061
            (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...
2062
        );
2063
2064
        $urlsegment = SiteTreeURLSegmentField::create("URLSegment", $this->fieldLabel('URLSegment'))
2065
            ->setURLPrefix($baseLink)
2066
            ->setDefaultURL($this->generateURLSegment(_t(
2067
                'CMSMain.NEWPAGE',
2068
                array('pagetype' => $this->i18n_singular_name())
0 ignored issues
show
Documentation introduced by
array('pagetype' => $this->i18n_singular_name()) is of type array<string,string,{"pagetype":"string"}>, but the function expects a string.

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
2069
            )));
2070
        $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...
2071
            ? $this->fieldLabel('LinkChangeNote')
2072
            : '';
2073
        if (!Config::inst()->get('SilverStripe\\View\\Parsers\\URLSegmentFilter', 'default_allow_multibyte')) {
2074
            $helpText .= _t('SiteTreeURLSegmentField.HelpChars', ' Special characters are automatically converted or removed.');
2075
        }
2076
        $urlsegment->setHelpText($helpText);
2077
2078
        $fields = new FieldList(
2079
            $rootTab = new TabSet(
2080
                "Root",
2081
                $tabMain = new Tab(
2082
                    'Main',
2083
                    new TextField("Title", $this->fieldLabel('Title')),
2084
                    $urlsegment,
2085
                    new TextField("MenuTitle", $this->fieldLabel('MenuTitle')),
2086
                    $htmlField = new HTMLEditorField("Content", _t('SiteTree.HTMLEDITORTITLE', "Content", 'HTML editor title')),
2087
                    ToggleCompositeField::create(
2088
                        'Metadata',
2089
                        _t('SiteTree.MetadataToggle', 'Metadata'),
2090
                        array(
2091
                            $metaFieldDesc = new TextareaField("MetaDescription", $this->fieldLabel('MetaDescription')),
2092
                            $metaFieldExtra = new TextareaField("ExtraMeta", $this->fieldLabel('ExtraMeta'))
2093
                        )
2094
                    )->setHeadingLevel(4)
2095
                ),
2096
                $tabDependent = new Tab(
2097
                    'Dependent',
2098
                    $dependentNote,
2099
                    $dependentTable
2100
                )
2101
            )
2102
        );
2103
        $htmlField->addExtraClass('stacked');
2104
2105
        // Help text for MetaData on page content editor
2106
        $metaFieldDesc
2107
            ->setRightTitle(
2108
                _t(
2109
                    'SiteTree.METADESCHELP',
2110
                    "Search engines use this content for displaying search results (although it will not influence their ranking)."
2111
                )
2112
            )
2113
            ->addExtraClass('help');
2114
        $metaFieldExtra
2115
            ->setRightTitle(
2116
                _t(
2117
                    'SiteTree.METAEXTRAHELP',
2118
                    "HTML tags for additional meta information. For example &lt;meta name=\"customName\" content=\"your custom content here\" /&gt;"
2119
                )
2120
            )
2121
            ->addExtraClass('help');
2122
2123
        // Conditional dependent pages tab
2124
        if ($dependentPagesCount) {
2125
            $tabDependent->setTitle(_t('SiteTree.TABDEPENDENT', "Dependent pages") . " ($dependentPagesCount)");
2126
        } else {
2127
            $fields->removeFieldFromTab('Root', 'Dependent');
2128
        }
2129
2130
        $tabMain->setTitle(_t('SiteTree.TABCONTENT', "Main Content"));
2131
2132
        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...
2133
            $obsoleteWarning = _t(
2134
                'SiteTree.OBSOLETECLASS',
2135
                "This page is of obsolete type {type}. Saving will reset its type and you may lose data",
2136
                array('type' => $this->ObsoleteClassName)
0 ignored issues
show
Bug introduced by
The property ObsoleteClassName does not seem to exist. Did you mean ClassName?

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

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

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

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

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

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

Let’s take a look at an example:

class Author {
    private $name;

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

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

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

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

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

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

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

Loading history...
3035
    }
3036
3037
    /**
3038
     * Returns 'root' if the current page has no parent, or 'subpage' otherwise
3039
     *
3040
     * @return string
3041
     */
3042
    public function getParentType()
3043
    {
3044
        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...
3045
    }
3046
3047
    /**
3048
     * Clear the permissions cache for SiteTree
3049
     */
3050
    public static function reset()
3051
    {
3052
        self::$cache_permissions = array();
3053
    }
3054
3055
    public static function on_db_reset()
3056
    {
3057
        self::$cache_permissions = array();
3058
    }
3059
}
3060