Completed
Push — master ( 0fbdad...a03f6b )
by Daniel
16:02
created

code/Model/SiteTree.php (1 issue)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
3
namespace SilverStripe\CMS\Model;
4
5
use Page;
6
use SilverStripe\CampaignAdmin\AddToCampaignHandler_FormAction;
7
use SilverStripe\CMS\Controllers\CMSPageEditController;
8
use SilverStripe\CMS\Controllers\ContentController;
9
use SilverStripe\CMS\Controllers\ModelAsController;
10
use SilverStripe\CMS\Controllers\RootURLController;
11
use SilverStripe\CMS\Forms\SiteTreeURLSegmentField;
12
use SilverStripe\Control\ContentNegotiator;
13
use SilverStripe\Control\Controller;
14
use SilverStripe\Control\Director;
15
use SilverStripe\Control\RequestHandler;
16
use SilverStripe\Core\ClassInfo;
17
use SilverStripe\Core\Config\Config;
18
use SilverStripe\Core\Convert;
19
use SilverStripe\Core\Injector\Injector;
20
use SilverStripe\Core\Resettable;
21
use SilverStripe\Dev\Deprecation;
22
use SilverStripe\Forms\CheckboxField;
23
use SilverStripe\Forms\CompositeField;
24
use SilverStripe\Forms\DropdownField;
25
use SilverStripe\Forms\FieldGroup;
26
use SilverStripe\Forms\FieldList;
27
use SilverStripe\Forms\FormAction;
28
use SilverStripe\Forms\GridField\GridField;
29
use SilverStripe\Forms\GridField\GridFieldDataColumns;
30
use SilverStripe\Forms\HTMLEditor\HTMLEditorField;
31
use SilverStripe\Forms\ListboxField;
32
use SilverStripe\Forms\LiteralField;
33
use SilverStripe\Forms\OptionsetField;
34
use SilverStripe\Forms\Tab;
35
use SilverStripe\Forms\TabSet;
36
use SilverStripe\Forms\TextareaField;
37
use SilverStripe\Forms\TextField;
38
use SilverStripe\Forms\ToggleCompositeField;
39
use SilverStripe\Forms\TreeDropdownField;
40
use SilverStripe\i18n\i18n;
41
use SilverStripe\i18n\i18nEntityProvider;
42
use SilverStripe\ORM\ArrayList;
43
use SilverStripe\ORM\CMSPreviewable;
44
use SilverStripe\ORM\DataList;
45
use SilverStripe\ORM\DataObject;
46
use SilverStripe\ORM\DB;
47
use SilverStripe\ORM\HiddenClass;
48
use SilverStripe\ORM\Hierarchy\Hierarchy;
49
use SilverStripe\ORM\ManyManyList;
50
use SilverStripe\ORM\ValidationResult;
51
use SilverStripe\Security\Group;
52
use SilverStripe\Security\InheritedPermissions;
53
use SilverStripe\Security\InheritedPermissionsExtension;
54
use SilverStripe\Security\Member;
55
use SilverStripe\Security\Permission;
56
use SilverStripe\Security\PermissionChecker;
57
use SilverStripe\Security\PermissionProvider;
58
use SilverStripe\Security\Security;
59
use SilverStripe\SiteConfig\SiteConfig;
60
use SilverStripe\Versioned\Versioned;
61
use SilverStripe\View\ArrayData;
62
use SilverStripe\View\HTML;
63
use SilverStripe\View\Parsers\ShortcodeParser;
64
use SilverStripe\View\Parsers\URLSegmentFilter;
65
use SilverStripe\View\SSViewer;
66
use Subsite;
67
68
/**
69
 * Basic data-object representing all pages within the site tree. All page types that live within the hierarchy should
70
 * inherit from this. In addition, it contains a number of static methods for querying the site tree and working with
71
 * draft and published states.
72
 *
73
 * <h2>URLs</h2>
74
 * A page is identified during request handling via its "URLSegment" database column. As pages can be nested, the full
75
 * path of a URL might contain multiple segments. Each segment is stored in its filtered representation (through
76
 * {@link URLSegmentFilter}). The full path is constructed via {@link Link()}, {@link RelativeLink()} and
77
 * {@link AbsoluteLink()}. You can allow these segments to contain multibyte characters through
78
 * {@link URLSegmentFilter::$default_allow_multibyte}.
79
 *
80
 * @property string URLSegment
81
 * @property string Title
82
 * @property string MenuTitle
83
 * @property string Content HTML content of the page.
84
 * @property string MetaDescription
85
 * @property string ExtraMeta
86
 * @property string ShowInMenus
87
 * @property string ShowInSearch
88
 * @property string Sort Integer value denoting the sort order.
89
 * @property string ReportClass
90
 *
91
 * @method ManyManyList ViewerGroups() List of groups that can view this object.
92
 * @method ManyManyList EditorGroups() List of groups that can edit this object.
93
 * @method SiteTree Parent()
94
 *
95
 * @mixin Hierarchy
96
 * @mixin Versioned
97
 * @mixin SiteTreeLinkTracking
98
 * @mixin InheritedPermissionsExtension
99
 */
100
class SiteTree extends DataObject implements PermissionProvider, i18nEntityProvider, CMSPreviewable, Resettable
101
{
102
103
    /**
104
     * Indicates what kind of children this page type can have.
105
     * This can be an array of allowed child classes, or the string "none" -
106
     * indicating that this page type can't have children.
107
     * If a classname is prefixed by "*", such as "*Page", then only that
108
     * class is allowed - no subclasses. Otherwise, the class and all its
109
     * subclasses are allowed.
110
     * To control allowed children on root level (no parent), use {@link $can_be_root}.
111
     *
112
     * Note that this setting is cached when used in the CMS, use the "flush" query parameter to clear it.
113
     *
114
     * @config
115
     * @var array
116
     */
117
    private static $allowed_children = [
118
        self::class
119
    ];
120
121
    /**
122
     * The default child class for this page.
123
     * Note: Value might be cached, see {@link $allowed_chilren}.
124
     *
125
     * @config
126
     * @var string
127
     */
128
    private static $default_child = "Page";
129
130
    /**
131
     * Default value for SiteTree.ClassName enum
132
     * {@see DBClassName::getDefault}
133
     *
134
     * @config
135
     * @var string
136
     */
137
    private static $default_classname = "Page";
138
139
    /**
140
     * The default parent class for this page.
141
     * Note: Value might be cached, see {@link $allowed_chilren}.
142
     *
143
     * @config
144
     * @var string
145
     */
146
    private static $default_parent = null;
147
148
    /**
149
     * Controls whether a page can be in the root of the site tree.
150
     * Note: Value might be cached, see {@link $allowed_chilren}.
151
     *
152
     * @config
153
     * @var bool
154
     */
155
    private static $can_be_root = true;
156
157
    /**
158
     * List of permission codes a user can have to allow a user to create a page of this type.
159
     * Note: Value might be cached, see {@link $allowed_chilren}.
160
     *
161
     * @config
162
     * @var array
163
     */
164
    private static $need_permission = null;
165
166
    /**
167
     * If you extend a class, and don't want to be able to select the old class
168
     * in the cms, set this to the old class name. Eg, if you extended Product
169
     * to make ImprovedProduct, then you would set $hide_ancestor to Product.
170
     *
171
     * @config
172
     * @var string
173
     */
174
    private static $hide_ancestor = null;
175
176
    private static $db = array(
177
        "URLSegment" => "Varchar(255)",
178
        "Title" => "Varchar(255)",
179
        "MenuTitle" => "Varchar(100)",
180
        "Content" => "HTMLText",
181
        "MetaDescription" => "Text",
182
        "ExtraMeta" => "HTMLFragment(['whitelist' => ['meta', 'link']])",
183
        "ShowInMenus" => "Boolean",
184
        "ShowInSearch" => "Boolean",
185
        "Sort" => "Int",
186
        "HasBrokenFile" => "Boolean",
187
        "HasBrokenLink" => "Boolean",
188
        "ReportClass" => "Varchar",
189
    );
190
191
    private static $indexes = array(
192
        "URLSegment" => true,
193
    );
194
195
    private static $has_many = array(
196
        "VirtualPages" => "SilverStripe\\CMS\\Model\\VirtualPage.CopyContentFrom"
197
    );
198
199
    private static $owned_by = array(
200
        "VirtualPages"
201
    );
202
203
    private static $casting = array(
204
        "Breadcrumbs" => "HTMLFragment",
205
        "LastEdited" => "Datetime",
206
        "Created" => "Datetime",
207
        'Link' => 'Text',
208
        'RelativeLink' => 'Text',
209
        'AbsoluteLink' => 'Text',
210
        'CMSEditLink' => 'Text',
211
        'TreeTitle' => 'HTMLFragment',
212
        'MetaTags' => 'HTMLFragment',
213
    );
214
215
    private static $defaults = array(
216
        "ShowInMenus" => 1,
217
        "ShowInSearch" => 1,
218
    );
219
220
    private static $table_name = 'SiteTree';
221
222
    private static $versioning = array(
223
        "Stage",  "Live"
224
    );
225
226
    private static $default_sort = "\"Sort\"";
227
228
    /**
229
     * If this is false, the class cannot be created in the CMS by regular content authors, only by ADMINs.
230
     * @var boolean
231
     * @config
232
     */
233
    private static $can_create = true;
234
235
    /**
236
     * Icon to use in the CMS page tree. This should be the full filename, relative to the webroot.
237
     * Also supports custom CSS rule contents (applied to the correct selector for the tree UI implementation).
238
     *
239
     * @see CMSMain::generateTreeStylingCSS()
240
     * @config
241
     * @var string
242
     */
243
    private static $icon = null;
244
245
    private static $extensions = [
246
        Hierarchy::class,
247
        Versioned::class,
248
        SiteTreeLinkTracking::class,
249
        InheritedPermissionsExtension::class,
250
    ];
251
252
    private static $searchable_fields = array(
253
        'Title',
254
        'Content',
255
    );
256
257
    private static $field_labels = array(
258
        'URLSegment' => 'URL'
259
    );
260
261
    /**
262
     * @config
263
     */
264
    private static $nested_urls = true;
265
266
    /**
267
     * @config
268
    */
269
    private static $create_default_pages = true;
270
271
    /**
272
     * This controls whether of not extendCMSFields() is called by getCMSFields.
273
     */
274
    private static $runCMSFieldsExtensions = true;
275
276
    /**
277
     * @config
278
     * @var boolean
279
     */
280
    private static $enforce_strict_hierarchy = true;
281
282
    /**
283
     * The value used for the meta generator tag. Leave blank to omit the tag.
284
     *
285
     * @config
286
     * @var string
287
     */
288
    private static $meta_generator = 'SilverStripe - http://silverstripe.org';
289
290
    protected $_cache_statusFlags = null;
291
292
    /**
293
     * Plural form for SiteTree / Page classes. Not inherited by subclasses.
294
     *
295
     * @config
296
     * @var string
297
     */
298
    private static $base_plural_name = 'Pages';
299
300
    /**
301
     * Plural form for SiteTree / Page classes. Not inherited by subclasses.
302
     *
303
     * @config
304
     * @var string
305
     */
306
    private static $base_singular_name = 'Page';
307
308
    /**
309
     * Description of the class functionality, typically shown to a user
310
     * when selecting which page type to create. Translated through {@link provideI18nEntities()}.
311
     *
312
     * @see SiteTree::classDescription()
313
     * @see SiteTree::i18n_classDescription()
314
     *
315
     * @config
316
     * @var string
317
     */
318
    private static $description = null;
319
320
    /**
321
     * Description for Page and SiteTree classes, but not inherited by subclasses.
322
     * override SiteTree::$description in subclasses instead.
323
     *
324
     * @see SiteTree::classDescription()
325
     * @see SiteTree::i18n_classDescription()
326
     *
327
     * @config
328
     * @var string
329
     */
330
    private static $base_description = 'Generic content page';
331
332
    /**
333
     * Fetches the {@link SiteTree} object that maps to a link.
334
     *
335
     * If you have enabled {@link SiteTree::config()->nested_urls} on this site, then you can use a nested link such as
336
     * "about-us/staff/", and this function will traverse down the URL chain and grab the appropriate link.
337
     *
338
     * Note that if no model can be found, this method will fall over to a extended alternateGetByLink method provided
339
     * by a extension attached to {@link SiteTree}
340
     *
341
     * @param string $link  The link of the page to search for
342
     * @param bool   $cache True (default) to use caching, false to force a fresh search from the database
343
     * @return SiteTree
344
     */
345
    public static function get_by_link($link, $cache = true)
346
    {
347
        if (trim($link, '/')) {
348
            $link = trim(Director::makeRelative($link), '/');
349
        } else {
350
            $link = RootURLController::get_homepage_link();
351
        }
352
353
        $parts = preg_split('|/+|', $link);
354
355
        // Grab the initial root level page to traverse down from.
356
        $URLSegment = array_shift($parts);
357
        $conditions = array('"SiteTree"."URLSegment"' => rawurlencode($URLSegment));
358
        if (self::config()->nested_urls) {
359
            $conditions[] = array('"SiteTree"."ParentID"' => 0);
360
        }
361
        /** @var SiteTree $sitetree */
362
        $sitetree = DataObject::get_one(self::class, $conditions, $cache);
363
364
        /// Fall back on a unique URLSegment for b/c.
365
        if (!$sitetree
366
            && self::config()->nested_urls
367
            && $sitetree = DataObject::get_one(self::class, array(
368
                '"SiteTree"."URLSegment"' => $URLSegment
369
            ), $cache)
370
        ) {
371
            return $sitetree;
372
        }
373
374
        // Attempt to grab an alternative page from extensions.
375
        if (!$sitetree) {
376
            $parentID = self::config()->nested_urls ? 0 : null;
377
378 View Code Duplication
            if ($alternatives = static::singleton()->extend('alternateGetByLink', $URLSegment, $parentID)) {
379
                foreach ($alternatives as $alternative) {
380
                    if ($alternative) {
381
                        $sitetree = $alternative;
382
                    }
383
                }
384
            }
385
386
            if (!$sitetree) {
387
                return null;
388
            }
389
        }
390
391
        // Check if we have any more URL parts to parse.
392
        if (!self::config()->nested_urls || !count($parts)) {
393
            return $sitetree;
394
        }
395
396
        // Traverse down the remaining URL segments and grab the relevant SiteTree objects.
397
        foreach ($parts as $segment) {
398
            $next = DataObject::get_one(
399
                self::class,
400
                array(
401
                    '"SiteTree"."URLSegment"' => $segment,
402
                    '"SiteTree"."ParentID"' => $sitetree->ID
403
                ),
404
                $cache
405
            );
406
407
            if (!$next) {
408
                $parentID = (int) $sitetree->ID;
409
410 View Code Duplication
                if ($alternatives = static::singleton()->extend('alternateGetByLink', $segment, $parentID)) {
411
                    foreach ($alternatives as $alternative) {
412
                        if ($alternative) {
413
                            $next = $alternative;
414
                        }
415
                    }
416
                }
417
418
                if (!$next) {
419
                    return null;
420
                }
421
            }
422
423
            $sitetree->destroy();
424
            $sitetree = $next;
425
        }
426
427
        return $sitetree;
428
    }
429
430
    /**
431
     * Return a subclass map of SiteTree that shouldn't be hidden through {@link SiteTree::$hide_ancestor}
432
     *
433
     * @return array
434
     */
435
    public static function page_type_classes()
436
    {
437
        $classes = ClassInfo::getValidSubClasses();
438
439
        $baseClassIndex = array_search(self::class, $classes);
440
        if ($baseClassIndex !== false) {
441
            unset($classes[$baseClassIndex]);
442
        }
443
444
        $kill_ancestors = array();
445
446
        // figure out if there are any classes we don't want to appear
447
        foreach ($classes as $class) {
448
            $instance = singleton($class);
449
450
            // do any of the progeny want to hide an ancestor?
451
            if ($ancestor_to_hide = $instance->stat('hide_ancestor')) {
452
                // note for killing later
453
                $kill_ancestors[] = $ancestor_to_hide;
454
            }
455
        }
456
457
        // If any of the descendents don't want any of the elders to show up, cruelly render the elders surplus to
458
        // requirements
459
        if ($kill_ancestors) {
460
            $kill_ancestors = array_unique($kill_ancestors);
461
            foreach ($kill_ancestors as $mark) {
462
                // unset from $classes
463
                $idx = array_search($mark, $classes, true);
464
                if ($idx !== false) {
465
                    unset($classes[$idx]);
466
                }
467
            }
468
        }
469
470
        return $classes;
471
    }
472
473
    /**
474
     * Replace a "[sitetree_link id=n]" shortcode with a link to the page with the corresponding ID.
475
     *
476
     * @param array      $arguments
477
     * @param string     $content
478
     * @param ShortcodeParser $parser
479
     * @return string
480
     */
481
    public static function link_shortcode_handler($arguments, $content = null, $parser = null)
482
    {
483
        if (!isset($arguments['id']) || !is_numeric($arguments['id'])) {
484
            return null;
485
        }
486
487
        /** @var SiteTree $page */
488
        if (!($page = DataObject::get_by_id(self::class, $arguments['id']))         // Get the current page by ID.
489
            && !($page = Versioned::get_latest_version(self::class, $arguments['id'])) // Attempt link to old version.
490
        ) {
491
             return null; // There were no suitable matches at all.
492
        }
493
494
        /** @var SiteTree $page */
495
        $link = Convert::raw2att($page->Link());
496
497
        if ($content) {
498
            return sprintf('<a href="%s">%s</a>', $link, $parser->parse($content));
499
        } else {
500
            return $link;
501
        }
502
    }
503
504
    /**
505
     * Return the link for this {@link SiteTree} object, with the {@link Director::baseURL()} included.
506
     *
507
     * @param string $action Optional controller action (method).
508
     *                       Note: URI encoding of this parameter is applied automatically through template casting,
509
     *                       don't encode the passed parameter. Please use {@link Controller::join_links()} instead to
510
     *                       append GET parameters.
511
     * @return string
512
     */
513
    public function Link($action = null)
514
    {
515
        return Controller::join_links(Director::baseURL(), $this->RelativeLink($action));
516
    }
517
518
    /**
519
     * Get the absolute URL for this page, including protocol and host.
520
     *
521
     * @param string $action See {@link Link()}
522
     * @return string
523
     */
524
    public function AbsoluteLink($action = null)
525
    {
526
        if ($this->hasMethod('alternateAbsoluteLink')) {
527
            return $this->alternateAbsoluteLink($action);
528
        } else {
529
            return Director::absoluteURL($this->Link($action));
530
        }
531
    }
532
533
    /**
534
     * Base link used for previewing. Defaults to absolute URL, in order to account for domain changes, e.g. on multi
535
     * site setups. Does not contain hints about the stage, see {@link SilverStripeNavigator} for details.
536
     *
537
     * @param string $action See {@link Link()}
538
     * @return string
539
     */
540
    public function PreviewLink($action = null)
541
    {
542
        if ($this->hasMethod('alternatePreviewLink')) {
543
            Deprecation::notice('5.0', 'Use updatePreviewLink or override PreviewLink method');
544
            return $this->alternatePreviewLink($action);
545
        }
546
547
        $link = $this->AbsoluteLink($action);
548
        $this->extend('updatePreviewLink', $link, $action);
549
        return $link;
550
    }
551
552
    public function getMimeType()
553
    {
554
        return 'text/html';
555
    }
556
557
    /**
558
     * Return the link for this {@link SiteTree} object relative to the SilverStripe root.
559
     *
560
     * By default, if this page is the current home page, and there is no action specified then this will return a link
561
     * to the root of the site. However, if you set the $action parameter to TRUE then the link will not be rewritten
562
     * and returned in its full form.
563
     *
564
     * @uses RootURLController::get_homepage_link()
565
     *
566
     * @param string $action See {@link Link()}
567
     * @return string
568
     */
569
    public function RelativeLink($action = null)
570
    {
571
        if ($this->ParentID && self::config()->nested_urls) {
572
            $parent = $this->Parent();
573
            // If page is removed select parent from version history (for archive page view)
574
            if ((!$parent || !$parent->exists()) && !$this->isOnDraft()) {
575
                $parent = Versioned::get_latest_version(self::class, $this->ParentID);
576
            }
577
            $base = $parent->RelativeLink($this->URLSegment);
578
        } elseif (!$action && $this->URLSegment == RootURLController::get_homepage_link()) {
579
            // Unset base for root-level homepages.
580
            // Note: Homepages with action parameters (or $action === true)
581
            // need to retain their URLSegment.
582
            $base = null;
583
        } else {
584
            $base = $this->URLSegment;
585
        }
586
587
        $this->extend('updateRelativeLink', $base, $action);
588
589
        // Legacy support: If $action === true, retain URLSegment for homepages,
590
        // but don't append any action
591
        if ($action === true) {
592
            $action = null;
593
        }
594
595
        return Controller::join_links($base, '/', $action);
596
    }
597
598
    /**
599
     * Get the absolute URL for this page on the Live site.
600
     *
601
     * @param bool $includeStageEqualsLive Whether to append the URL with ?stage=Live to force Live mode
602
     * @return string
603
     */
604
    public function getAbsoluteLiveLink($includeStageEqualsLive = true)
605
    {
606
        $oldReadingMode = Versioned::get_reading_mode();
607
        Versioned::set_stage(Versioned::LIVE);
608
        /** @var SiteTree $live */
609
        $live = Versioned::get_one_by_stage(self::class, Versioned::LIVE, array(
610
            '"SiteTree"."ID"' => $this->ID
611
        ));
612
        if ($live) {
613
            $link = $live->AbsoluteLink();
614
            if ($includeStageEqualsLive) {
615
                $link = Controller::join_links($link, '?stage=Live');
616
            }
617
        } else {
618
            $link = null;
619
        }
620
621
        Versioned::set_reading_mode($oldReadingMode);
622
        return $link;
623
    }
624
625
    /**
626
     * Generates a link to edit this page in the CMS.
627
     *
628
     * @return string
629
     */
630
    public function CMSEditLink()
631
    {
632
        $link = Controller::join_links(
633
            CMSPageEditController::singleton()->Link('show'),
634
            $this->ID
635
        );
636
        return Director::absoluteURL($link);
637
    }
638
639
640
    /**
641
     * Return a CSS identifier generated from this page's link.
642
     *
643
     * @return string The URL segment
644
     */
645
    public function ElementName()
646
    {
647
        return str_replace('/', '-', trim($this->RelativeLink(true), '/'));
648
    }
649
650
    /**
651
     * Returns true if this is the currently active page being used to handle this request.
652
     *
653
     * @return bool
654
     */
655
    public function isCurrent()
656
    {
657
        $currentPage = Director::get_current_page();
658
        if ($currentPage instanceof ContentController) {
659
            $currentPage = $currentPage->data();
660
        }
661
        if ($currentPage instanceof SiteTree) {
662
            return $currentPage === $this || $currentPage->ID === $this->ID;
663
        }
664
        return false;
665
    }
666
667
    /**
668
     * Check if this page is in the currently active section (e.g. it is either current or one of its children is
669
     * currently being viewed).
670
     *
671
     * @return bool
672
     */
673
    public function isSection()
674
    {
675
        return $this->isCurrent() || (
676
            Director::get_current_page() instanceof SiteTree && in_array($this->ID, Director::get_current_page()->getAncestors()->column())
677
        );
678
    }
679
680
    /**
681
     * Check if the parent of this page has been removed (or made otherwise unavailable), and is still referenced by
682
     * this child. Any such orphaned page may still require access via the CMS, but should not be shown as accessible
683
     * to external users.
684
     *
685
     * @return bool
686
     */
687
    public function isOrphaned()
688
    {
689
        // Always false for root pages
690
        if (empty($this->ParentID)) {
691
            return false;
692
        }
693
694
        // Parent must exist and not be an orphan itself
695
        $parent = $this->Parent();
696
        return !$parent || !$parent->exists() || $parent->isOrphaned();
697
    }
698
699
    /**
700
     * Return "link" or "current" depending on if this is the {@link SiteTree::isCurrent()} current page.
701
     *
702
     * @return string
703
     */
704
    public function LinkOrCurrent()
705
    {
706
        return $this->isCurrent() ? 'current' : 'link';
707
    }
708
709
    /**
710
     * Return "link" or "section" depending on if this is the {@link SiteTree::isSeciton()} current section.
711
     *
712
     * @return string
713
     */
714
    public function LinkOrSection()
715
    {
716
        return $this->isSection() ? 'section' : 'link';
717
    }
718
719
    /**
720
     * Return "link", "current" or "section" depending on if this page is the current page, or not on the current page
721
     * but in the current section.
722
     *
723
     * @return string
724
     */
725
    public function LinkingMode()
726
    {
727
        if ($this->isCurrent()) {
728
            return 'current';
729
        } elseif ($this->isSection()) {
730
            return 'section';
731
        } else {
732
            return 'link';
733
        }
734
    }
735
736
    /**
737
     * Check if this page is in the given current section.
738
     *
739
     * @param string $sectionName Name of the section to check
740
     * @return bool True if we are in the given section
741
     */
742
    public function InSection($sectionName)
743
    {
744
        $page = Director::get_current_page();
745
        while ($page && $page->exists()) {
746
            if ($sectionName == $page->URLSegment) {
747
                return true;
748
            }
749
            $page = $page->Parent();
750
        }
751
        return false;
752
    }
753
754
    /**
755
     * Reset Sort on duped page
756
     *
757
     * @param SiteTree $original
758
     * @param bool $doWrite
759
     */
760
    public function onBeforeDuplicate($original, $doWrite)
761
    {
762
        $this->Sort = 0;
763
    }
764
765
    /**
766
     * Duplicates each child of this node recursively and returns the top-level duplicate node.
767
     *
768
     * @return static The duplicated object
769
     */
770
    public function duplicateWithChildren()
771
    {
772
        /** @var SiteTree $clone */
773
        $clone = $this->duplicate();
774
        $children = $this->AllChildren();
775
776
        if ($children) {
777
            /** @var SiteTree $child */
778
            $sort = 0;
779
            foreach ($children as $child) {
780
                $childClone = method_exists($child, 'duplicateWithChildren')
781
                    ? $child->duplicateWithChildren()
782
                    : $child->duplicate();
783
                $childClone->ParentID = $clone->ID;
784
                //retain sort order by manually setting sort values
785
                $childClone->Sort = ++$sort;
786
                $childClone->write();
787
            }
788
        }
789
790
        return $clone;
791
    }
792
793
    /**
794
     * Duplicate this node and its children as a child of the node with the given ID
795
     *
796
     * @param int $id ID of the new node's new parent
797
     */
798
    public function duplicateAsChild($id)
799
    {
800
        /** @var SiteTree $newSiteTree */
801
        $newSiteTree = $this->duplicate();
802
        $newSiteTree->ParentID = $id;
803
        $newSiteTree->Sort = 0;
804
        $newSiteTree->write();
805
    }
806
807
    /**
808
     * Return a breadcrumb trail to this page. Excludes "hidden" pages (with ShowInMenus=0) by default.
809
     *
810
     * @param int $maxDepth The maximum depth to traverse.
811
     * @param boolean $unlinked Whether to link page titles.
812
     * @param boolean|string $stopAtPageType ClassName of a page to stop the upwards traversal.
813
     * @param boolean $showHidden Include pages marked with the attribute ShowInMenus = 0
814
     * @return string The breadcrumb trail.
815
     */
816
    public function Breadcrumbs($maxDepth = 20, $unlinked = false, $stopAtPageType = false, $showHidden = false, $delimiter = '&raquo;')
817
    {
818
        $pages = $this->getBreadcrumbItems($maxDepth, $stopAtPageType, $showHidden);
819
        $template = SSViewer::create('BreadcrumbsTemplate');
820
        return $template->process($this->customise(new ArrayData(array(
821
            "Pages" => $pages,
822
            "Unlinked" => $unlinked,
823
            "Delimiter" => $delimiter,
824
        ))));
825
    }
826
827
828
    /**
829
     * Returns a list of breadcrumbs for the current page.
830
     *
831
     * @param int $maxDepth The maximum depth to traverse.
832
     * @param boolean|string $stopAtPageType ClassName of a page to stop the upwards traversal.
833
     * @param boolean $showHidden Include pages marked with the attribute ShowInMenus = 0
834
     *
835
     * @return ArrayList
836
    */
837
    public function getBreadcrumbItems($maxDepth = 20, $stopAtPageType = false, $showHidden = false)
838
    {
839
        $page = $this;
840
        $pages = array();
841
842
        while ($page
843
            && $page->exists()
844
            && (!$maxDepth || count($pages) < $maxDepth)
845
            && (!$stopAtPageType || $page->ClassName != $stopAtPageType)
846
        ) {
847
            if ($showHidden || $page->ShowInMenus || ($page->ID == $this->ID)) {
848
                $pages[] = $page;
849
            }
850
851
            $page = $page->Parent();
852
        }
853
854
        return new ArrayList(array_reverse($pages));
855
    }
856
857
858
    /**
859
     * Make this page a child of another page.
860
     *
861
     * If the parent page does not exist, resolve it to a valid ID before updating this page's reference.
862
     *
863
     * @param SiteTree|int $item Either the parent object, or the parent ID
864
     */
865
    public function setParent($item)
866
    {
867
        if (is_object($item)) {
868
            if (!$item->exists()) {
869
                $item->write();
870
            }
871
            $this->setField("ParentID", $item->ID);
872
        } else {
873
            $this->setField("ParentID", $item);
874
        }
875
    }
876
877
    /**
878
     * Get the parent of this page.
879
     *
880
     * @return SiteTree Parent of this page
881
     */
882
    public function getParent()
883
    {
884
        if ($parentID = $this->getField("ParentID")) {
885
            return DataObject::get_by_id(self::class, $parentID);
886
        }
887
        return null;
888
    }
889
890
    /**
891
     * Return a string of the form "parent - page" or "grandparent - parent - page" using page titles
892
     *
893
     * @param int $level The maximum amount of levels to traverse.
894
     * @param string $separator Seperating string
895
     * @return string The resulting string
896
     */
897
    public function NestedTitle($level = 2, $separator = " - ")
898
    {
899
        $item = $this;
900
        $parts = [];
901
        while ($item && $level > 0) {
902
            $parts[] = $item->Title;
903
            $item = $item->getParent();
904
            $level--;
905
        }
906
        return implode($separator, array_reverse($parts));
907
    }
908
909
    /**
910
     * This function should return true if the current user can execute this action. It can be overloaded to customise
911
     * the security model for an application.
912
     *
913
     * Slightly altered from parent behaviour in {@link DataObject->can()}:
914
     * - Checks for existence of a method named "can<$perm>()" on the object
915
     * - Calls decorators and only returns for FALSE "vetoes"
916
     * - Falls back to {@link Permission::check()}
917
     * - Does NOT check for many-many relations named "Can<$perm>"
918
     *
919
     * @uses DataObjectDecorator->can()
920
     *
921
     * @param string $perm The permission to be checked, such as 'View'
922
     * @param Member $member The member whose permissions need checking. Defaults to the currently logged in user.
923
     * @param array $context Context argument for canCreate()
924
     * @return bool True if the the member is allowed to do the given action
925
     */
926
    public function can($perm, $member = null, $context = array())
927
    {
928
        if (!$member) {
929
            $member = Security::getCurrentUser();
930
        }
931
932
        if ($member && Permission::checkMember($member, "ADMIN")) {
933
            return true;
934
        }
935
936
        if (is_string($perm) && method_exists($this, 'can' . ucfirst($perm))) {
937
            $method = 'can' . ucfirst($perm);
938
            return $this->$method($member);
939
        }
940
941
        $results = $this->extend('can', $member);
942
        if ($results && is_array($results)) {
943
            if (!min($results)) {
944
                return false;
945
            }
946
        }
947
948
        return ($member && Permission::checkMember($member, $perm));
949
    }
950
951
    /**
952
     * This function should return true if the current user can add children to this page. It can be overloaded to
953
     * customise the security model for an application.
954
     *
955
     * Denies permission if any of the following conditions is true:
956
     * - alternateCanAddChildren() on a extension returns false
957
     * - canEdit() is not granted
958
     * - There are no classes defined in {@link $allowed_children}
959
     *
960
     * @uses SiteTreeExtension->canAddChildren()
961
     * @uses canEdit()
962
     * @uses $allowed_children
963
     *
964
     * @param Member|int $member
965
     * @return bool True if the current user can add children
966
     */
967
    public function canAddChildren($member = null)
968
    {
969
        // Disable adding children to archived pages
970
        if (!$this->isOnDraft()) {
971
            return false;
972
        }
973
974
        if (!$member) {
975
            $member = Security::getCurrentUser();
976
        }
977
978
        // Standard mechanism for accepting permission changes from extensions
979
        $extended = $this->extendedCan('canAddChildren', $member);
980
        if ($extended !== null) {
981
            return $extended;
982
        }
983
984
        // Default permissions
985
        if ($member && Permission::checkMember($member, "ADMIN")) {
986
            return true;
987
        }
988
989
        return $this->canEdit($member) && $this->stat('allowed_children') !== 'none';
990
    }
991
992
    /**
993
     * This function should return true if the current user can view this page. It can be overloaded to customise the
994
     * security model for an application.
995
     *
996
     * Denies permission if any of the following conditions is true:
997
     * - canView() on any extension returns false
998
     * - "CanViewType" directive is set to "Inherit" and any parent page return false for canView()
999
     * - "CanViewType" directive is set to "LoggedInUsers" and no user is logged in
1000
     * - "CanViewType" directive is set to "OnlyTheseUsers" and user is not in the given groups
1001
     *
1002
     * @uses DataExtension->canView()
1003
     * @uses ViewerGroups()
1004
     *
1005
     * @param Member $member
1006
     * @return bool True if the current user can view this page
1007
     */
1008
    public function canView($member = null)
1009
    {
1010
        if (!$member) {
1011
            $member = Security::getCurrentUser();
1012
        }
1013
1014
        // Standard mechanism for accepting permission changes from extensions
1015
        $extended = $this->extendedCan('canView', $member);
1016
        if ($extended !== null) {
1017
            return $extended;
1018
        }
1019
1020
        // admin override
1021
        if ($member && Permission::checkMember($member, array("ADMIN", "SITETREE_VIEW_ALL"))) {
1022
            return true;
1023
        }
1024
1025
        // Orphaned pages (in the current stage) are unavailable, except for admins via the CMS
1026
        if ($this->isOrphaned()) {
1027
            return false;
1028
        }
1029
1030
        // Note: getInheritedPermissions() is disused in this instance
1031
        // to allow parent canView extensions to influence subpage canView()
1032
1033
        // check for empty spec
1034
        if (!$this->CanViewType || $this->CanViewType === InheritedPermissions::ANYONE) {
1035
            return true;
1036
        }
1037
1038
        // check for inherit
1039
        if ($this->CanViewType === InheritedPermissions::INHERIT) {
1040
            if ($this->ParentID) {
1041
                return $this->Parent()->canView($member);
1042
            } else {
1043
                return $this->getSiteConfig()->canViewPages($member);
1044
            }
1045
        }
1046
1047
        // check for any logged-in users
1048
        if ($this->CanViewType === InheritedPermissions::LOGGED_IN_USERS && $member && $member->ID) {
1049
            return true;
1050
        }
1051
1052
        // check for specific groups
1053
        if ($this->CanViewType === InheritedPermissions::ONLY_THESE_USERS
1054
            && $member
1055
            && $member->inGroups($this->ViewerGroups())
1056
        ) {
1057
            return true;
1058
        }
1059
1060
        return false;
1061
    }
1062
1063
    /**
1064
     * Check if this page can be published
1065
     *
1066
     * @param Member $member
1067
     * @return bool
1068
     */
1069 View Code Duplication
    public function canPublish($member = null)
0 ignored issues
show
This method seems to be duplicated in your project.

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

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

Loading history...
1070
    {
1071
        if (!$member) {
1072
            $member = Security::getCurrentUser();
1073
        }
1074
1075
        // Check extension
1076
        $extended = $this->extendedCan('canPublish', $member);
1077
        if ($extended !== null) {
1078
            return $extended;
1079
        }
1080
1081
        if (Permission::checkMember($member, "ADMIN")) {
1082
            return true;
1083
        }
1084
1085
        // Default to relying on edit permission
1086
        return $this->canEdit($member);
1087
    }
1088
1089
    /**
1090
     * This function should return true if the current user can delete this page. It can be overloaded to customise the
1091
     * security model for an application.
1092
     *
1093
     * Denies permission if any of the following conditions is true:
1094
     * - canDelete() returns false on any extension
1095
     * - canEdit() returns false
1096
     * - any descendant page returns false for canDelete()
1097
     *
1098
     * @uses canDelete()
1099
     * @uses SiteTreeExtension->canDelete()
1100
     * @uses canEdit()
1101
     *
1102
     * @param Member $member
1103
     * @return bool True if the current user can delete this page
1104
     */
1105
    public function canDelete($member = null)
1106
    {
1107
        if (!$member) {
1108
            $member = Security::getCurrentUser();
1109
        }
1110
1111
        // Standard mechanism for accepting permission changes from extensions
1112
        $extended = $this->extendedCan('canDelete', $member);
1113
        if ($extended !== null) {
1114
            return $extended;
1115
        }
1116
1117
        if (!$member) {
1118
            return false;
1119
        }
1120
1121
        // Default permission check
1122
        if (Permission::checkMember($member, array("ADMIN", "SITETREE_EDIT_ALL"))) {
1123
            return true;
1124
        }
1125
1126
        // Check inherited permissions
1127
        return static::getPermissionChecker()
1128
            ->canDelete($this->ID, $member);
1129
    }
1130
1131
    /**
1132
     * This function should return true if the current user can create new pages of this class, regardless of class. It
1133
     * can be overloaded to customise the security model for an application.
1134
     *
1135
     * By default, permission to create at the root level is based on the SiteConfig configuration, and permission to
1136
     * create beneath a parent is based on the ability to edit that parent page.
1137
     *
1138
     * Use {@link canAddChildren()} to control behaviour of creating children under this page.
1139
     *
1140
     * @uses $can_create
1141
     * @uses DataExtension->canCreate()
1142
     *
1143
     * @param Member $member
1144
     * @param array $context Optional array which may contain array('Parent' => $parentObj)
1145
     *                       If a parent page is known, it will be checked for validity.
1146
     *                       If omitted, it will be assumed this is to be created as a top level page.
1147
     * @return bool True if the current user can create pages on this class.
1148
     */
1149
    public function canCreate($member = null, $context = array())
1150
    {
1151
        if (!$member) {
1152
            $member = Security::getCurrentUser();
1153
        }
1154
1155
        // Check parent (custom canCreate option for SiteTree)
1156
        // Block children not allowed for this parent type
1157
        $parent = isset($context['Parent']) ? $context['Parent'] : null;
1158
        if ($parent && !in_array(static::class, $parent->allowedChildren())) {
1159
            return false;
1160
        }
1161
1162
        // Standard mechanism for accepting permission changes from extensions
1163
        $extended = $this->extendedCan(__FUNCTION__, $member, $context);
1164
        if ($extended !== null) {
1165
            return $extended;
1166
        }
1167
1168
        // Check permission
1169
        if ($member && Permission::checkMember($member, "ADMIN")) {
1170
            return true;
1171
        }
1172
1173
        // Fall over to inherited permissions
1174
        if ($parent && $parent->exists()) {
1175
            return $parent->canAddChildren($member);
1176
        } else {
1177
            // This doesn't necessarily mean we are creating a root page, but that
1178
            // we don't know if there is a parent, so default to this permission
1179
            return SiteConfig::current_site_config()->canCreateTopLevel($member);
1180
        }
1181
    }
1182
1183
    /**
1184
     * This function should return true if the current user can edit this page. It can be overloaded to customise the
1185
     * security model for an application.
1186
     *
1187
     * Denies permission if any of the following conditions is true:
1188
     * - canEdit() on any extension returns false
1189
     * - canView() return false
1190
     * - "CanEditType" directive is set to "Inherit" and any parent page return false for canEdit()
1191
     * - "CanEditType" directive is set to "LoggedInUsers" and no user is logged in or doesn't have the
1192
     *   CMS_Access_CMSMAIN permission code
1193
     * - "CanEditType" directive is set to "OnlyTheseUsers" and user is not in the given groups
1194
     *
1195
     * @uses canView()
1196
     * @uses EditorGroups()
1197
     * @uses DataExtension->canEdit()
1198
     *
1199
     * @param Member $member Set to false if you want to explicitly test permissions without a valid user (useful for
1200
     *                       unit tests)
1201
     * @return bool True if the current user can edit this page
1202
     */
1203 View Code Duplication
    public function canEdit($member = null)
1204
    {
1205
        if (!$member) {
1206
            $member = Security::getCurrentUser();
1207
        }
1208
1209
        // Standard mechanism for accepting permission changes from extensions
1210
        $extended = $this->extendedCan('canEdit', $member);
1211
        if ($extended !== null) {
1212
            return $extended;
1213
        }
1214
1215
        // Default permissions
1216
        if (Permission::checkMember($member, "SITETREE_EDIT_ALL")) {
1217
            return true;
1218
        }
1219
1220
        // Check inherited permissions
1221
        return static::getPermissionChecker()
1222
            ->canEdit($this->ID, $member);
1223
    }
1224
1225
    /**
1226
     * Stub method to get the site config, unless the current class can provide an alternate.
1227
     *
1228
     * @return SiteConfig
1229
     */
1230
    public function getSiteConfig()
1231
    {
1232
        $configs = $this->invokeWithExtensions('alternateSiteConfig');
1233
        foreach (array_filter($configs) as $config) {
1234
            return $config;
1235
        }
1236
1237
        return SiteConfig::current_site_config();
1238
    }
1239
1240
    /**
1241
     * @return PermissionChecker
1242
     */
1243
    public static function getPermissionChecker()
1244
    {
1245
        return Injector::inst()->get(PermissionChecker::class.'.sitetree');
1246
    }
1247
1248
    /**
1249
     * Collate selected descendants of this page.
1250
     *
1251
     * {@link $condition} will be evaluated on each descendant, and if it is succeeds, that item will be added to the
1252
     * $collator array.
1253
     *
1254
     * @param string $condition The PHP condition to be evaluated. The page will be called $item
1255
     * @param array  $collator  An array, passed by reference, to collect all of the matching descendants.
1256
     * @return bool
1257
     */
1258
    public function collateDescendants($condition, &$collator)
1259
    {
1260
        // apply reasonable hierarchy limits
1261
        $threshold = Config::inst()->get(Hierarchy::class, 'node_threshold_leaf');
1262
        if ($this->numChildren() > $threshold) {
1263
            return false;
1264
        }
1265
1266
        $children = $this->Children();
1267
        if ($children) {
1268
            foreach ($children as $item) {
1269
                if (eval("return $condition;")) {
1270
                    $collator[] = $item;
1271
                }
1272
                /** @var SiteTree $item */
1273
                $item->collateDescendants($condition, $collator);
1274
            }
1275
            return true;
1276
        }
1277
        return false;
1278
    }
1279
1280
    /**
1281
     * Return the title, description, keywords and language metatags.
1282
     *
1283
     * @todo Move <title> tag in separate getter for easier customization and more obvious usage
1284
     *
1285
     * @param bool $includeTitle Show default <title>-tag, set to false for custom templating
1286
     * @return string The XHTML metatags
1287
     */
1288
    public function MetaTags($includeTitle = true)
1289
    {
1290
        $tags = array();
1291
        if ($includeTitle && strtolower($includeTitle) != 'false') {
1292
            $tags[] = HTML::createTag('title', array(), $this->obj('Title')->forTemplate());
1293
        }
1294
1295
        $generator = trim(Config::inst()->get(self::class, 'meta_generator'));
1296
        if (!empty($generator)) {
1297
            $tags[] = HTML::createTag('meta', array(
1298
                'name' => 'generator',
1299
                'content' => $generator,
1300
            ));
1301
        }
1302
1303
        $charset = ContentNegotiator::config()->uninherited('encoding');
1304
        $tags[] = HTML::createTag('meta', array(
1305
            'http-equiv' => 'Content-Type',
1306
            'content' => 'text/html; charset=' . $charset,
1307
        ));
1308
        if ($this->MetaDescription) {
1309
            $tags[] = HTML::createTag('meta', array(
1310
                'name' => 'description',
1311
                'content' => $this->MetaDescription,
1312
            ));
1313
        }
1314
1315
        if (Permission::check('CMS_ACCESS_CMSMain')
1316
            && $this->ID > 0
1317
        ) {
1318
            $tags[] = HTML::createTag('meta', array(
1319
                'name' => 'x-page-id',
1320
                'content' => $this->obj('ID')->forTemplate(),
1321
            ));
1322
            $tags[] = HTML::createTag('meta', array(
1323
                'name' => 'x-cms-edit-link',
1324
                'content' => $this->obj('CMSEditLink')->forTemplate(),
1325
            ));
1326
        }
1327
1328
        $tags = implode("\n", $tags);
1329
        if ($this->ExtraMeta) {
1330
            $tags .= $this->obj('ExtraMeta')->forTemplate();
1331
        }
1332
1333
        $this->extend('MetaTags', $tags);
1334
1335
        return $tags;
1336
    }
1337
1338
    /**
1339
     * Returns the object that contains the content that a user would associate with this page.
1340
     *
1341
     * Ordinarily, this is just the page itself, but for example on RedirectorPages or VirtualPages ContentSource() will
1342
     * return the page that is linked to.
1343
     *
1344
     * @return $this
1345
     */
1346
    public function ContentSource()
1347
    {
1348
        return $this;
1349
    }
1350
1351
    /**
1352
     * Add default records to database.
1353
     *
1354
     * This function is called whenever the database is built, after the database tables have all been created. Overload
1355
     * this to add default records when the database is built, but make sure you call parent::requireDefaultRecords().
1356
     */
1357
    public function requireDefaultRecords()
1358
    {
1359
        parent::requireDefaultRecords();
1360
1361
        // default pages
1362
        if (static::class == self::class && $this->config()->create_default_pages) {
1363
            if (!SiteTree::get_by_link(RootURLController::config()->default_homepage_link)) {
1364
                $homepage = new Page();
1365
                $homepage->Title = _t(__CLASS__.'.DEFAULTHOMETITLE', 'Home');
1366
                $homepage->Content = _t(__CLASS__.'.DEFAULTHOMECONTENT', '<p>Welcome to SilverStripe! This is the default homepage. You can edit this page by opening <a href="admin/">the CMS</a>.</p><p>You can now access the <a href="http://docs.silverstripe.org">developer documentation</a>, or begin the <a href="http://www.silverstripe.org/learn/lessons">SilverStripe lessons</a>.</p>');
1367
                $homepage->URLSegment = RootURLController::config()->default_homepage_link;
1368
                $homepage->Sort = 1;
1369
                $homepage->write();
1370
                $homepage->copyVersionToStage(Versioned::DRAFT, Versioned::LIVE);
1371
                $homepage->flushCache();
1372
                DB::alteration_message('Home page created', 'created');
1373
            }
1374
1375
            if (DB::query("SELECT COUNT(*) FROM \"SiteTree\"")->value() == 1) {
1376
                $aboutus = new Page();
1377
                $aboutus->Title = _t(__CLASS__.'.DEFAULTABOUTTITLE', 'About Us');
1378
                $aboutus->Content = _t(
1379
                    'SilverStripe\\CMS\\Model\\SiteTree.DEFAULTABOUTCONTENT',
1380
                    '<p>You can fill this page out with your own content, or delete it and create your own pages.</p>'
1381
                );
1382
                $aboutus->Sort = 2;
1383
                $aboutus->write();
1384
                $aboutus->copyVersionToStage(Versioned::DRAFT, Versioned::LIVE);
1385
                $aboutus->flushCache();
1386
                DB::alteration_message('About Us page created', 'created');
1387
1388
                $contactus = new Page();
1389
                $contactus->Title = _t(__CLASS__.'.DEFAULTCONTACTTITLE', 'Contact Us');
1390
                $contactus->Content = _t(
1391
                    'SilverStripe\\CMS\\Model\\SiteTree.DEFAULTCONTACTCONTENT',
1392
                    '<p>You can fill this page out with your own content, or delete it and create your own pages.</p>'
1393
                );
1394
                $contactus->Sort = 3;
1395
                $contactus->write();
1396
                $contactus->copyVersionToStage(Versioned::DRAFT, Versioned::LIVE);
1397
                $contactus->flushCache();
1398
                DB::alteration_message('Contact Us page created', 'created');
1399
            }
1400
        }
1401
    }
1402
1403
    protected function onBeforeWrite()
1404
    {
1405
        parent::onBeforeWrite();
1406
1407
        // If Sort hasn't been set, make this page come after it's siblings
1408
        if (!$this->Sort) {
1409
            $parentID = ($this->ParentID) ? $this->ParentID : 0;
1410
            $this->Sort = DB::prepared_query(
1411
                "SELECT MAX(\"Sort\") + 1 FROM \"SiteTree\" WHERE \"ParentID\" = ?",
1412
                array($parentID)
1413
            )->value();
1414
        }
1415
1416
        // If there is no URLSegment set, generate one from Title
1417
        $defaultSegment = $this->generateURLSegment(_t(
1418
            'SilverStripe\\CMS\\Controllers\\CMSMain.NEWPAGE',
1419
            'New {pagetype}',
1420
            array('pagetype' => $this->i18n_singular_name())
1421
        ));
1422
        if ((!$this->URLSegment || $this->URLSegment == $defaultSegment) && $this->Title) {
1423
            $this->URLSegment = $this->generateURLSegment($this->Title);
1424
        } elseif ($this->isChanged('URLSegment', 2)) {
1425
            // Do a strict check on change level, to avoid double encoding caused by
1426
            // bogus changes through forceChange()
1427
            $filter = URLSegmentFilter::create();
1428
            $this->URLSegment = $filter->filter($this->URLSegment);
1429
            // If after sanitising there is no URLSegment, give it a reasonable default
1430
            if (!$this->URLSegment) {
1431
                $this->URLSegment = "page-$this->ID";
1432
            }
1433
        }
1434
1435
        // Ensure that this object has a non-conflicting URLSegment value.
1436
        $count = 2;
1437
        while (!$this->validURLSegment()) {
1438
            $this->URLSegment = preg_replace('/-[0-9]+$/', null, $this->URLSegment) . '-' . $count;
1439
            $count++;
1440
        }
1441
1442
        $this->syncLinkTracking();
1443
1444
        // Check to see if we've only altered fields that shouldn't affect versioning
1445
        $fieldsIgnoredByVersioning = array('HasBrokenLink', 'Status', 'HasBrokenFile', 'ToDo', 'VersionID', 'SaveCount');
1446
        $changedFields = array_keys($this->getChangedFields(true, 2));
1447
1448
        // This more rigorous check is inline with the test that write() does to decide whether or not to write to the
1449
        // DB. We use that to avoid cluttering the system with a migrateVersion() call that doesn't get used
1450
        $oneChangedFields = array_keys($this->getChangedFields(true, 1));
1451
1452
        if ($oneChangedFields && !array_diff($changedFields, $fieldsIgnoredByVersioning)) {
1453
            // This will have the affect of preserving the versioning
1454
            $this->migrateVersion($this->Version);
1455
        }
1456
    }
1457
1458
    /**
1459
     * Trigger synchronisation of link tracking
1460
     *
1461
     * {@see SiteTreeLinkTracking::augmentSyncLinkTracking}
1462
     */
1463
    public function syncLinkTracking()
1464
    {
1465
        $this->extend('augmentSyncLinkTracking');
1466
    }
1467
1468
    public function onBeforeDelete()
1469
    {
1470
        parent::onBeforeDelete();
1471
1472
        // If deleting this page, delete all its children.
1473
        if (SiteTree::config()->enforce_strict_hierarchy && $children = $this->AllChildren()) {
1474
            foreach ($children as $child) {
1475
                /** @var SiteTree $child */
1476
                $child->delete();
1477
            }
1478
        }
1479
    }
1480
1481
    public function onAfterDelete()
1482
    {
1483
        $this->updateDependentPages();
1484
        parent::onAfterDelete();
1485
    }
1486
1487
    public function flushCache($persistent = true)
1488
    {
1489
        parent::flushCache($persistent);
1490
        $this->_cache_statusFlags = null;
1491
    }
1492
1493
    public function validate()
1494
    {
1495
        $result = parent::validate();
1496
1497
        // Allowed children validation
1498
        $parent = $this->getParent();
1499
        if ($parent && $parent->exists()) {
1500
            // No need to check for subclasses or instanceof, as allowedChildren() already
1501
            // deconstructs any inheritance trees already.
1502
            $allowed = $parent->allowedChildren();
1503
            $subject = ($this instanceof VirtualPage && $this->CopyContentFromID)
1504
                ? $this->CopyContentFrom()
1505
                : $this;
1506
            if (!in_array($subject->ClassName, $allowed)) {
1507
                $result->addError(
1508
                    _t(
1509
                        'SilverStripe\\CMS\\Model\\SiteTree.PageTypeNotAllowed',
1510
                        'Page type "{type}" not allowed as child of this parent page',
1511
                        array('type' => $subject->i18n_singular_name())
1512
                    ),
1513
                    ValidationResult::TYPE_ERROR,
1514
                    'ALLOWED_CHILDREN'
1515
                );
1516
            }
1517
        }
1518
1519
        // "Can be root" validation
1520
        if (!$this->stat('can_be_root') && !$this->ParentID) {
1521
            $result->addError(
1522
                _t(
1523
                    'SilverStripe\\CMS\\Model\\SiteTree.PageTypNotAllowedOnRoot',
1524
                    'Page type "{type}" is not allowed on the root level',
1525
                    array('type' => $this->i18n_singular_name())
1526
                ),
1527
                ValidationResult::TYPE_ERROR,
1528
                'CAN_BE_ROOT'
1529
            );
1530
        }
1531
1532
        return $result;
1533
    }
1534
1535
    /**
1536
     * Returns true if this object has a URLSegment value that does not conflict with any other objects. This method
1537
     * checks for:
1538
     *  - A page with the same URLSegment that has a conflict
1539
     *  - Conflicts with actions on the parent page
1540
     *  - A conflict caused by a root page having the same URLSegment as a class name
1541
     *
1542
     * @return bool
1543
     */
1544
    public function validURLSegment()
1545
    {
1546
        if (self::config()->nested_urls && $parent = $this->Parent()) {
1547
            if ($controller = ModelAsController::controller_for($parent)) {
1548
                if ($controller instanceof Controller && $controller->hasAction($this->URLSegment)) {
1549
                    return false;
1550
                }
1551
            }
1552
        }
1553
1554
        if (!self::config()->nested_urls || !$this->ParentID) {
1555
            if (class_exists($this->URLSegment) && is_subclass_of($this->URLSegment, RequestHandler::class)) {
1556
                return false;
1557
            }
1558
        }
1559
1560
        // Filters by url, id, and parent
1561
        $filter = array('"SiteTree"."URLSegment"' => $this->URLSegment);
1562
        if ($this->ID) {
1563
            $filter['"SiteTree"."ID" <> ?'] = $this->ID;
1564
        }
1565
        if (self::config()->nested_urls) {
1566
            $filter['"SiteTree"."ParentID"'] = $this->ParentID ? $this->ParentID : 0;
1567
        }
1568
1569
        // If any of the extensions return `0` consider the segment invalid
1570
        $extensionResponses = array_filter(
1571
            (array)$this->extend('augmentValidURLSegment'),
1572
            function ($response) {
1573
                return !is_null($response);
1574
            }
1575
        );
1576
        if ($extensionResponses) {
1577
            return min($extensionResponses);
1578
        }
1579
1580
        // Check existence
1581
        return !DataObject::get(self::class, $filter)->exists();
1582
    }
1583
1584
    /**
1585
     * Generate a URL segment based on the title provided.
1586
     *
1587
     * If {@link Extension}s wish to alter URL segment generation, they can do so by defining
1588
     * updateURLSegment(&$url, $title).  $url will be passed by reference and should be modified. $title will contain
1589
     * the title that was originally used as the source of this generated URL. This lets extensions either start from
1590
     * scratch, or incrementally modify the generated URL.
1591
     *
1592
     * @param string $title Page title
1593
     * @return string Generated url segment
1594
     */
1595
    public function generateURLSegment($title)
1596
    {
1597
        $filter = URLSegmentFilter::create();
1598
        $t = $filter->filter($title);
1599
1600
        // Fallback to generic page name if path is empty (= no valid, convertable characters)
1601
        if (!$t || $t == '-' || $t == '-1') {
1602
            $t = "page-$this->ID";
1603
        }
1604
1605
        // Hook for extensions
1606
        $this->extend('updateURLSegment', $t, $title);
1607
1608
        return $t;
1609
    }
1610
1611
    /**
1612
     * Gets the URL segment for the latest draft version of this page.
1613
     *
1614
     * @return string
1615
     */
1616
    public function getStageURLSegment()
1617
    {
1618
        $stageRecord = Versioned::get_one_by_stage(self::class, Versioned::DRAFT, array(
1619
            '"SiteTree"."ID"' => $this->ID
1620
        ));
1621
        return ($stageRecord) ? $stageRecord->URLSegment : null;
1622
    }
1623
1624
    /**
1625
     * Gets the URL segment for the currently published version of this page.
1626
     *
1627
     * @return string
1628
     */
1629
    public function getLiveURLSegment()
1630
    {
1631
        $liveRecord = Versioned::get_one_by_stage(self::class, Versioned::LIVE, array(
1632
            '"SiteTree"."ID"' => $this->ID
1633
        ));
1634
        return ($liveRecord) ? $liveRecord->URLSegment : null;
1635
    }
1636
1637
    /**
1638
     * Returns the pages that depend on this page. This includes virtual pages, pages that link to it, etc.
1639
     *
1640
     * @param bool $includeVirtuals Set to false to exlcude virtual pages.
1641
     * @return ArrayList
1642
     */
1643
    public function DependentPages($includeVirtuals = true)
1644
    {
1645
        if (class_exists('Subsite')) {
1646
            $origDisableSubsiteFilter = Subsite::$disable_subsite_filter;
1647
            Subsite::disable_subsite_filter(true);
1648
        }
1649
1650
        // Content links
1651
        $items = new ArrayList();
1652
1653
        // We merge all into a regular SS_List, because DataList doesn't support merge
1654
        if ($contentLinks = $this->BackLinkTracking()) {
1655
            $linkList = new ArrayList();
1656
            foreach ($contentLinks as $item) {
1657
                $item->DependentLinkType = 'Content link';
1658
                $linkList->push($item);
1659
            }
1660
            $items->merge($linkList);
1661
        }
1662
1663
        // Virtual pages
1664
        if ($includeVirtuals) {
1665
            $virtuals = $this->VirtualPages();
1666
            if ($virtuals) {
1667
                $virtualList = new ArrayList();
1668
                foreach ($virtuals as $item) {
1669
                    $item->DependentLinkType = 'Virtual page';
1670
                    $virtualList->push($item);
1671
                }
1672
                $items->merge($virtualList);
1673
            }
1674
        }
1675
1676
        // Redirector pages
1677
        $redirectors = RedirectorPage::get()->where(array(
1678
            '"RedirectorPage"."RedirectionType"' => 'Internal',
1679
            '"RedirectorPage"."LinkToID"' => $this->ID
1680
        ));
1681
        if ($redirectors) {
1682
            $redirectorList = new ArrayList();
1683
            foreach ($redirectors as $item) {
1684
                $item->DependentLinkType = 'Redirector page';
1685
                $redirectorList->push($item);
1686
            }
1687
            $items->merge($redirectorList);
1688
        }
1689
1690
        if (class_exists('Subsite')) {
1691
            Subsite::disable_subsite_filter($origDisableSubsiteFilter);
1692
        }
1693
1694
        return $items;
1695
    }
1696
1697
    /**
1698
     * Return all virtual pages that link to this page.
1699
     *
1700
     * @return DataList
1701
     */
1702
    public function VirtualPages()
1703
    {
1704
        $pages = parent::VirtualPages();
1705
1706
        // Disable subsite filter for these pages
1707
        if ($pages instanceof DataList) {
1708
            return $pages->setDataQueryParam('Subsite.filter', false);
1709
        } else {
1710
            return $pages;
1711
        }
1712
    }
1713
1714
    /**
1715
     * Returns a FieldList with which to create the main editing form.
1716
     *
1717
     * You can override this in your child classes to add extra fields - first get the parent fields using
1718
     * parent::getCMSFields(), then use addFieldToTab() on the FieldList.
1719
     *
1720
     * See {@link getSettingsFields()} for a different set of fields concerned with configuration aspects on the record,
1721
     * e.g. access control.
1722
     *
1723
     * @return FieldList The fields to be displayed in the CMS
1724
     */
1725
    public function getCMSFields()
1726
    {
1727
        // Status / message
1728
        // Create a status message for multiple parents
1729
        if ($this->ID && is_numeric($this->ID)) {
1730
            $linkedPages = $this->VirtualPages();
1731
1732
            $parentPageLinks = array();
1733
1734
            if ($linkedPages->count() > 0) {
1735
                /** @var VirtualPage $linkedPage */
1736
                foreach ($linkedPages as $linkedPage) {
1737
                    $parentPage = $linkedPage->Parent();
1738
                    if ($parentPage && $parentPage->exists()) {
1739
                        $link = Convert::raw2att($parentPage->CMSEditLink());
1740
                        $title = Convert::raw2xml($parentPage->Title);
1741
                    } else {
1742
                        $link = CMSPageEditController::singleton()->Link('show');
1743
                        $title = _t(__CLASS__.'.TOPLEVEL', 'Site Content (Top Level)');
1744
                    }
1745
                    $parentPageLinks[] = "<a class=\"cmsEditlink\" href=\"{$link}\">{$title}</a>";
1746
                }
1747
1748
                $lastParent = array_pop($parentPageLinks);
1749
                $parentList = "'$lastParent'";
1750
1751
                if (count($parentPageLinks)) {
1752
                    $parentList = "'" . implode("', '", $parentPageLinks) . "' and "
1753
                        . $parentList;
1754
                }
1755
1756
                $statusMessage[] = _t(
1757
                    'SilverStripe\\CMS\\Model\\SiteTree.APPEARSVIRTUALPAGES',
1758
                    "This content also appears on the virtual pages in the {title} sections.",
1759
                    array('title' => $parentList)
1760
                );
1761
            }
1762
        }
1763
1764
        if ($this->HasBrokenLink || $this->HasBrokenFile) {
1765
            $statusMessage[] = _t(__CLASS__.'.HASBROKENLINKS', "This page has broken links.");
1766
        }
1767
1768
        $dependentNote = '';
1769
        $dependentTable = new LiteralField('DependentNote', '<p></p>');
1770
1771
        // Create a table for showing pages linked to this one
1772
        $dependentPages = $this->DependentPages();
1773
        $dependentPagesCount = $dependentPages->count();
1774
        if ($dependentPagesCount) {
1775
            $dependentColumns = array(
1776
                'Title' => $this->fieldLabel('Title'),
1777
                'AbsoluteLink' => _t(__CLASS__.'.DependtPageColumnURL', 'URL'),
1778
                'DependentLinkType' => _t(__CLASS__.'.DependtPageColumnLinkType', 'Link type'),
1779
            );
1780
            if (class_exists('Subsite')) {
1781
                $dependentColumns['Subsite.Title'] = singleton('Subsite')->i18n_singular_name();
1782
            }
1783
1784
            $dependentNote = new LiteralField('DependentNote', '<p>' . _t(__CLASS__.'.DEPENDENT_NOTE', 'The following pages depend on this page. This includes virtual pages, redirector pages, and pages with content links.') . '</p>');
1785
            $dependentTable = GridField::create(
1786
                'DependentPages',
1787
                false,
1788
                $dependentPages
1789
            );
1790
            /** @var GridFieldDataColumns $dataColumns */
1791
            $dataColumns = $dependentTable->getConfig()->getComponentByType('SilverStripe\\Forms\\GridField\\GridFieldDataColumns');
1792
            $dataColumns
1793
                ->setDisplayFields($dependentColumns)
1794
                ->setFieldFormatting(array(
1795
                    'Title' => function ($value, &$item) {
1796
                        return sprintf(
1797
                            '<a href="admin/pages/edit/show/%d">%s</a>',
1798
                            (int)$item->ID,
1799
                            Convert::raw2xml($item->Title)
1800
                        );
1801
                    },
1802
                    'AbsoluteLink' => function ($value, &$item) {
1803
                        return sprintf(
1804
                            '<a href="%s" target="_blank">%s</a>',
1805
                            Convert::raw2xml($value),
1806
                            Convert::raw2xml($value)
1807
                        );
1808
                    }
1809
                ));
1810
        }
1811
1812
        $baseLink = Controller::join_links(
1813
            Director::absoluteBaseURL(),
1814
            (self::config()->nested_urls && $this->ParentID ? $this->Parent()->RelativeLink(true) : null)
1815
        );
1816
1817
        $urlsegment = SiteTreeURLSegmentField::create("URLSegment", $this->fieldLabel('URLSegment'))
1818
            ->setURLPrefix($baseLink)
1819
            ->setDefaultURL($this->generateURLSegment(_t(
1820
                'SilverStripe\\CMS\\Controllers\\CMSMain.NEWPAGE',
1821
                'New {pagetype}',
1822
                array('pagetype' => $this->i18n_singular_name())
1823
            )));
1824
        $helpText = (self::config()->nested_urls && $this->numChildren())
1825
            ? $this->fieldLabel('LinkChangeNote')
1826
            : '';
1827
        if (!Config::inst()->get('SilverStripe\\View\\Parsers\\URLSegmentFilter', 'default_allow_multibyte')) {
1828
            $helpText .= _t('SilverStripe\\CMS\\Forms\\SiteTreeURLSegmentField.HelpChars', ' Special characters are automatically converted or removed.');
1829
        }
1830
        $urlsegment->setHelpText($helpText);
1831
1832
        $fields = new FieldList(
1833
            $rootTab = new TabSet(
1834
                "Root",
1835
                $tabMain = new Tab(
1836
                    'Main',
1837
                    new TextField("Title", $this->fieldLabel('Title')),
1838
                    $urlsegment,
1839
                    new TextField("MenuTitle", $this->fieldLabel('MenuTitle')),
1840
                    $htmlField = new HTMLEditorField("Content", _t(__CLASS__.'.HTMLEDITORTITLE', "Content", 'HTML editor title')),
1841
                    ToggleCompositeField::create(
1842
                        'Metadata',
1843
                        _t(__CLASS__.'.MetadataToggle', 'Metadata'),
1844
                        array(
1845
                            $metaFieldDesc = new TextareaField("MetaDescription", $this->fieldLabel('MetaDescription')),
1846
                            $metaFieldExtra = new TextareaField("ExtraMeta", $this->fieldLabel('ExtraMeta'))
1847
                        )
1848
                    )->setHeadingLevel(4)
1849
                ),
1850
                $tabDependent = new Tab(
1851
                    'Dependent',
1852
                    $dependentNote,
1853
                    $dependentTable
1854
                )
1855
            )
1856
        );
1857
        $htmlField->addExtraClass('stacked');
1858
1859
        // Help text for MetaData on page content editor
1860
        $metaFieldDesc
1861
            ->setRightTitle(
1862
                _t(
1863
                    'SilverStripe\\CMS\\Model\\SiteTree.METADESCHELP',
1864
                    "Search engines use this content for displaying search results (although it will not influence their ranking)."
1865
                )
1866
            )
1867
            ->addExtraClass('help');
1868
        $metaFieldExtra
1869
            ->setRightTitle(
1870
                _t(
1871
                    'SilverStripe\\CMS\\Model\\SiteTree.METAEXTRAHELP',
1872
                    "HTML tags for additional meta information. For example &lt;meta name=\"customName\" content=\"your custom content here\" /&gt;"
1873
                )
1874
            )
1875
            ->addExtraClass('help');
1876
1877
        // Conditional dependent pages tab
1878
        if ($dependentPagesCount) {
1879
            $tabDependent->setTitle(_t(__CLASS__.'.TABDEPENDENT', "Dependent pages") . " ($dependentPagesCount)");
1880
        } else {
1881
            $fields->removeFieldFromTab('Root', 'Dependent');
1882
        }
1883
1884
        $tabMain->setTitle(_t(__CLASS__.'.TABCONTENT', "Main Content"));
1885
1886
        if ($this->ObsoleteClassName) {
1887
            $obsoleteWarning = _t(
1888
                'SilverStripe\\CMS\\Model\\SiteTree.OBSOLETECLASS',
1889
                "This page is of obsolete type {type}. Saving will reset its type and you may lose data",
1890
                array('type' => $this->ObsoleteClassName)
1891
            );
1892
1893
            $fields->addFieldToTab(
1894
                "Root.Main",
1895
                new LiteralField("ObsoleteWarningHeader", "<p class=\"message warning\">$obsoleteWarning</p>"),
1896
                "Title"
1897
            );
1898
        }
1899
1900
        if (file_exists(BASE_PATH . '/install.php')) {
1901
            $fields->addFieldToTab("Root.Main", new LiteralField(
1902
                "InstallWarningHeader",
1903
                "<p class=\"message warning\">" . _t(
1904
                    "SilverStripe\\CMS\\Model\\SiteTree.REMOVE_INSTALL_WARNING",
1905
                    "Warning: You should remove install.php from this SilverStripe install for security reasons."
1906
                )
1907
                . "</p>"
1908
            ), "Title");
1909
        }
1910
1911
        if (self::$runCMSFieldsExtensions) {
1912
            $this->extend('updateCMSFields', $fields);
1913
        }
1914
1915
        return $fields;
1916
    }
1917
1918
1919
    /**
1920
     * Returns fields related to configuration aspects on this record, e.g. access control. See {@link getCMSFields()}
1921
     * for content-related fields.
1922
     *
1923
     * @return FieldList
1924
     */
1925
    public function getSettingsFields()
1926
    {
1927
        $mapFn = function ($groups = []) {
1928
            $map = [];
1929
            foreach ($groups as $group) {
1930
                // Listboxfield values are escaped, use ASCII char instead of &raquo;
1931
                $map[$group->ID] = $group->getBreadcrumbs(' > ');
1932
            }
1933
            asort($map);
1934
            return $map;
1935
        };
1936
        $groupsMap = $mapFn(Group::get());
1937
        $viewAllGroupsMap = $mapFn(Permission::get_groups_by_permission(['SITETREE_VIEW_ALL', 'ADMIN']));
1938
        $editAllGroupsMap = $mapFn(Permission::get_groups_by_permission(['SITETREE_EDIT_ALL', 'ADMIN']));
1939
1940
        $fields = new FieldList(
1941
            $rootTab = new TabSet(
1942
                "Root",
1943
                $tabBehaviour = new Tab(
1944
                    'Settings',
1945
                    new DropdownField(
1946
                        "ClassName",
1947
                        $this->fieldLabel('ClassName'),
1948
                        $this->getClassDropdown()
1949
                    ),
1950
                    $parentTypeSelector = new CompositeField(
1951
                        $parentType = new OptionsetField("ParentType", _t("SilverStripe\\CMS\\Model\\SiteTree.PAGELOCATION", "Page location"), array(
1952
                            "root" => _t("SilverStripe\\CMS\\Model\\SiteTree.PARENTTYPE_ROOT", "Top-level page"),
1953
                            "subpage" => _t("SilverStripe\\CMS\\Model\\SiteTree.PARENTTYPE_SUBPAGE", "Sub-page underneath a parent page"),
1954
                        )),
1955
                        $parentIDField = new TreeDropdownField("ParentID", $this->fieldLabel('ParentID'), self::class, 'ID', 'MenuTitle')
1956
                    ),
1957
                    $visibility = new FieldGroup(
1958
                        new CheckboxField("ShowInMenus", $this->fieldLabel('ShowInMenus')),
1959
                        new CheckboxField("ShowInSearch", $this->fieldLabel('ShowInSearch'))
1960
                    ),
1961
                    $viewersOptionsField = new OptionsetField(
1962
                        "CanViewType",
1963
                        _t(__CLASS__.'.ACCESSHEADER', "Who can view this page?")
1964
                    ),
1965
                    $viewerGroupsField = ListboxField::create("ViewerGroups", _t(__CLASS__.'.VIEWERGROUPS', "Viewer Groups"))
1966
                        ->setSource($groupsMap)
1967
                        ->setAttribute(
1968
                            'data-placeholder',
1969
                            _t(__CLASS__.'.GroupPlaceholder', 'Click to select group')
1970
                        ),
1971
                    $editorsOptionsField = new OptionsetField(
1972
                        "CanEditType",
1973
                        _t(__CLASS__.'.EDITHEADER', "Who can edit this page?")
1974
                    ),
1975
                    $editorGroupsField = ListboxField::create("EditorGroups", _t(__CLASS__.'.EDITORGROUPS', "Editor Groups"))
1976
                        ->setSource($groupsMap)
1977
                        ->setAttribute(
1978
                            'data-placeholder',
1979
                            _t(__CLASS__.'.GroupPlaceholder', 'Click to select group')
1980
                        )
1981
                )
1982
            )
1983
        );
1984
1985
        $parentType->addExtraClass('noborder');
1986
        $visibility->setTitle($this->fieldLabel('Visibility'));
1987
1988
1989
        // This filter ensures that the ParentID dropdown selection does not show this node,
1990
        // or its descendents, as this causes vanishing bugs
1991
        $parentIDField->setFilterFunction(function ($node) {
1992
            return $node->ID != $this->ID;
1993
        });
1994
        $parentTypeSelector->addExtraClass('parentTypeSelector');
1995
1996
        $tabBehaviour->setTitle(_t(__CLASS__.'.TABBEHAVIOUR', "Behavior"));
1997
1998
        // Make page location fields read-only if the user doesn't have the appropriate permission
1999
        if (!Permission::check("SITETREE_REORGANISE")) {
2000
            $fields->makeFieldReadonly('ParentType');
2001
            if ($this->getParentType() === 'root') {
2002
                $fields->removeByName('ParentID');
2003
            } else {
2004
                $fields->makeFieldReadonly('ParentID');
2005
            }
2006
        }
2007
2008
        $viewersOptionsSource = [
2009
            InheritedPermissions::INHERIT => _t(__CLASS__.'.INHERIT', "Inherit from parent page"),
2010
            InheritedPermissions::ANYONE => _t(__CLASS__.'.ACCESSANYONE', "Anyone"),
2011
            InheritedPermissions::LOGGED_IN_USERS => _t(__CLASS__.'.ACCESSLOGGEDIN', "Logged-in users"),
2012
            InheritedPermissions::ONLY_THESE_USERS => _t(
2013
                __CLASS__.'.ACCESSONLYTHESE',
2014
                "Only these groups (choose from list)"
2015
            ),
2016
        ];
2017
        $viewersOptionsField->setSource($viewersOptionsSource);
2018
2019
        // Editors have same options, except no "Anyone"
2020
        $editorsOptionsSource = $viewersOptionsSource;
2021
        unset($editorsOptionsSource[InheritedPermissions::ANYONE]);
2022
        $editorsOptionsField->setSource($editorsOptionsSource);
2023
2024
        if ($viewAllGroupsMap) {
2025
            $viewerGroupsField->setDescription(_t(
2026
                'SilverStripe\\CMS\\Model\\SiteTree.VIEWER_GROUPS_FIELD_DESC',
2027
                'Groups with global view permissions: {groupList}',
2028
                ['groupList' => implode(', ', array_values($viewAllGroupsMap))]
2029
            ));
2030
        }
2031
2032
        if ($editAllGroupsMap) {
2033
            $editorGroupsField->setDescription(_t(
2034
                'SilverStripe\\CMS\\Model\\SiteTree.EDITOR_GROUPS_FIELD_DESC',
2035
                'Groups with global edit permissions: {groupList}',
2036
                ['groupList' => implode(', ', array_values($editAllGroupsMap))]
2037
            ));
2038
        }
2039
2040
        if (!Permission::check('SITETREE_GRANT_ACCESS')) {
2041
            $fields->makeFieldReadonly($viewersOptionsField);
2042
            if ($this->CanEditType === InheritedPermissions::ONLY_THESE_USERS) {
2043
                $fields->makeFieldReadonly($viewerGroupsField);
2044
            } else {
2045
                $fields->removeByName('ViewerGroups');
2046
            }
2047
2048
            $fields->makeFieldReadonly($editorsOptionsField);
2049
            if ($this->CanEditType === InheritedPermissions::ONLY_THESE_USERS) {
2050
                $fields->makeFieldReadonly($editorGroupsField);
2051
            } else {
2052
                $fields->removeByName('EditorGroups');
2053
            }
2054
        }
2055
2056
        if (self::$runCMSFieldsExtensions) {
2057
            $this->extend('updateSettingsFields', $fields);
2058
        }
2059
2060
        return $fields;
2061
    }
2062
2063
    /**
2064
     * @param bool $includerelations A boolean value to indicate if the labels returned should include relation fields
2065
     * @return array
2066
     */
2067
    public function fieldLabels($includerelations = true)
2068
    {
2069
        $cacheKey = static::class . '_' . $includerelations;
2070
        if (!isset(self::$_cache_field_labels[$cacheKey])) {
2071
            $labels = parent::fieldLabels($includerelations);
2072
            $labels['Title'] = _t(__CLASS__.'.PAGETITLE', "Page name");
2073
            $labels['MenuTitle'] = _t(__CLASS__.'.MENUTITLE', "Navigation label");
2074
            $labels['MetaDescription'] = _t(__CLASS__.'.METADESC', "Meta Description");
2075
            $labels['ExtraMeta'] = _t(__CLASS__.'.METAEXTRA', "Custom Meta Tags");
2076
            $labels['ClassName'] = _t(__CLASS__.'.PAGETYPE', "Page type", 'Classname of a page object');
2077
            $labels['ParentType'] = _t(__CLASS__.'.PARENTTYPE', "Page location");
2078
            $labels['ParentID'] = _t(__CLASS__.'.PARENTID', "Parent page");
2079
            $labels['ShowInMenus'] =_t(__CLASS__.'.SHOWINMENUS', "Show in menus?");
2080
            $labels['ShowInSearch'] = _t(__CLASS__.'.SHOWINSEARCH', "Show in search?");
2081
            $labels['ViewerGroups'] = _t(__CLASS__.'.VIEWERGROUPS', "Viewer Groups");
2082
            $labels['EditorGroups'] = _t(__CLASS__.'.EDITORGROUPS', "Editor Groups");
2083
            $labels['URLSegment'] = _t(__CLASS__.'.URLSegment', 'URL Segment', 'URL for this page');
2084
            $labels['Content'] = _t(__CLASS__.'.Content', 'Content', 'Main HTML Content for a page');
2085
            $labels['CanViewType'] = _t(__CLASS__.'.Viewers', 'Viewers Groups');
2086
            $labels['CanEditType'] = _t(__CLASS__.'.Editors', 'Editors Groups');
2087
            $labels['Comments'] = _t(__CLASS__.'.Comments', 'Comments');
2088
            $labels['Visibility'] = _t(__CLASS__.'.Visibility', 'Visibility');
2089
            $labels['LinkChangeNote'] = _t(
2090
                'SilverStripe\\CMS\\Model\\SiteTree.LINKCHANGENOTE',
2091
                'Changing this page\'s link will also affect the links of all child pages.'
2092
            );
2093
2094
            if ($includerelations) {
2095
                $labels['Parent'] = _t(__CLASS__.'.has_one_Parent', 'Parent Page', 'The parent page in the site hierarchy');
2096
                $labels['LinkTracking'] = _t(__CLASS__.'.many_many_LinkTracking', 'Link Tracking');
2097
                $labels['ImageTracking'] = _t(__CLASS__.'.many_many_ImageTracking', 'Image Tracking');
2098
                $labels['BackLinkTracking'] = _t(__CLASS__.'.many_many_BackLinkTracking', 'Backlink Tracking');
2099
            }
2100
2101
            self::$_cache_field_labels[$cacheKey] = $labels;
2102
        }
2103
2104
        return self::$_cache_field_labels[$cacheKey];
2105
    }
2106
2107
    /**
2108
     * Get the actions available in the CMS for this page - eg Save, Publish.
2109
     *
2110
     * Frontend scripts and styles know how to handle the following FormFields:
2111
     * - top-level FormActions appear as standalone buttons
2112
     * - top-level CompositeField with FormActions within appear as grouped buttons
2113
     * - TabSet & Tabs appear as a drop ups
2114
     * - FormActions within the Tab are restyled as links
2115
     * - major actions can provide alternate states for richer presentation (see ssui.button widget extension)
2116
     *
2117
     * @return FieldList The available actions for this page.
2118
     */
2119
    public function getCMSActions()
2120
    {
2121
        // Get status of page
2122
        $isOnDraft = $this->isOnDraft();
2123
        $isPublished = $this->isPublished();
2124
        $stagesDiffer = $this->stagesDiffer(Versioned::DRAFT, Versioned::LIVE);
2125
2126
        // Check permissions
2127
        $canPublish = $this->canPublish();
2128
        $canUnpublish = $this->canUnpublish();
2129
        $canEdit = $this->canEdit();
2130
2131
        // Major actions appear as buttons immediately visible as page actions.
2132
        $majorActions = CompositeField::create()->setName('MajorActions');
2133
        $majorActions->setFieldHolderTemplate(get_class($majorActions) . '_holder_buttongroup');
2134
2135
        // Minor options are hidden behind a drop-up and appear as links (although they are still FormActions).
2136
        $rootTabSet = new TabSet('ActionMenus');
2137
        $moreOptions = new Tab(
2138
            'MoreOptions',
2139
            _t(__CLASS__.'.MoreOptions', 'More options', 'Expands a view for more buttons')
2140
        );
2141
        $moreOptions->addExtraClass('popover-actions-simulate');
2142
        $rootTabSet->push($moreOptions);
2143
        $rootTabSet->addExtraClass('ss-ui-action-tabset action-menus noborder');
2144
2145
        // Render page information into the "more-options" drop-up, on the top.
2146
        $liveRecord = Versioned::get_by_stage(self::class, Versioned::LIVE)->byID($this->ID);
2147
        $infoTemplate = SSViewer::get_templates_by_class(static::class, '_Information', self::class);
2148
        $moreOptions->push(
2149
            new LiteralField(
2150
                'Information',
2151
                $this->customise(array(
2152
                    'Live' => $liveRecord,
2153
                    'ExistsOnLive' => $isPublished
2154
                ))->renderWith($infoTemplate)
2155
            )
2156
        );
2157
2158
        // Add to campaign option if not-archived and has publish permission
2159
        if (($isPublished || $isOnDraft) && $canPublish) {
2160
            $moreOptions->push(
2161
                AddToCampaignHandler_FormAction::create()
2162
                    ->removeExtraClass('btn-primary')
2163
                    ->addExtraClass('btn-secondary')
2164
            );
2165
        }
2166
2167
        // "readonly"/viewing version that isn't the current version of the record
2168
        $stageRecord = Versioned::get_by_stage(static::class, Versioned::DRAFT)->byID($this->ID);
2169
        /** @skipUpgrade */
2170
        if ($stageRecord && $stageRecord->Version != $this->Version) {
2171
            $moreOptions->push(FormAction::create('email', _t('SilverStripe\\CMS\\Controllers\\CMSMain.EMAIL', 'Email')));
2172
            $moreOptions->push(FormAction::create('rollback', _t('SilverStripe\\CMS\\Controllers\\CMSMain.ROLLBACK', 'Roll back to this version')));
2173
            $actions = new FieldList(array($majorActions, $rootTabSet));
2174
2175
            // getCMSActions() can be extended with updateCMSActions() on a extension
2176
            $this->extend('updateCMSActions', $actions);
2177
            return $actions;
2178
        }
2179
2180
        // "unpublish"
2181 View Code Duplication
        if ($isPublished && $canPublish && $isOnDraft && $canUnpublish) {
2182
            $moreOptions->push(
2183
                FormAction::create('unpublish', _t(__CLASS__.'.BUTTONUNPUBLISH', 'Unpublish'), 'delete')
2184
                    ->setDescription(_t(__CLASS__.'.BUTTONUNPUBLISHDESC', 'Remove this page from the published site'))
2185
                    ->addExtraClass('btn-secondary')
2186
            );
2187
        }
2188
2189
        // "rollback"
2190 View Code Duplication
        if ($isOnDraft && $isPublished && $canEdit && $stagesDiffer) {
2191
            $moreOptions->push(
2192
                FormAction::create('rollback', _t(__CLASS__.'.BUTTONCANCELDRAFT', 'Cancel draft changes'))
2193
                    ->setDescription(_t(
2194
                        'SilverStripe\\CMS\\Model\\SiteTree.BUTTONCANCELDRAFTDESC',
2195
                        'Delete your draft and revert to the currently published page'
2196
                    ))
2197
                    ->addExtraClass('btn-secondary')
2198
            );
2199
        }
2200
2201
        // "restore"
2202
        if ($canEdit && !$isOnDraft && $isPublished) {
2203
            $majorActions->push(FormAction::create('revert', _t('SilverStripe\\CMS\\Controllers\\CMSMain.RESTORE', 'Restore')));
2204
        }
2205
2206
        // Check if we can restore a deleted page
2207
        // Note: It would be nice to have a canRestore() permission at some point
2208
        if ($canEdit && !$isOnDraft && !$isPublished) {
2209
            // Determine if we should force a restore to root (where once it was a subpage)
2210
            $restoreToRoot = $this->isParentArchived();
2211
2212
            // "restore"
2213
            $title = $restoreToRoot
2214
                ? _t('SilverStripe\\CMS\\Controllers\\CMSMain.RESTORE_TO_ROOT', 'Restore draft at top level')
2215
                : _t('SilverStripe\\CMS\\Controllers\\CMSMain.RESTORE', 'Restore draft');
2216
            $description = $restoreToRoot
2217
                ? _t('SilverStripe\\CMS\\Controllers\\CMSMain.RESTORE_TO_ROOT_DESC', 'Restore the archived version to draft as a top level page')
2218
                : _t('SilverStripe\\CMS\\Controllers\\CMSMain.RESTORE_DESC', 'Restore the archived version to draft');
2219
            $majorActions->push(
2220
                FormAction::create('restore', $title)
2221
                    ->setDescription($description)
2222
                    ->setAttribute('data-to-root', $restoreToRoot)
2223
                    ->setAttribute('data-icon', 'decline')
2224
            );
2225
        }
2226
2227
        // If a page is on any stage it can be archived
2228
        if (($isOnDraft || $isPublished) && $this->canArchive()) {
2229
            $title = $isPublished
2230
                ? _t('SilverStripe\\CMS\\Controllers\\CMSMain.UNPUBLISH_AND_ARCHIVE', 'Unpublish and archive')
2231
                : _t('SilverStripe\\CMS\\Controllers\\CMSMain.ARCHIVE', 'Archive');
2232
            $moreOptions->push(
2233
                FormAction::create('archive', $title)
2234
                    ->addExtraClass('delete btn btn-secondary')
2235
                    ->setDescription(_t(
2236
                        'SilverStripe\\CMS\\Model\\SiteTree.BUTTONDELETEDESC',
2237
                        'Remove from draft/live and send to archive'
2238
                    ))
2239
            );
2240
        }
2241
2242
        // "save", supports an alternate state that is still clickable, but notifies the user that the action is not needed.
2243
        if ($canEdit && $isOnDraft) {
2244
            $majorActions->push(
2245
                FormAction::create('save', _t(__CLASS__.'.BUTTONSAVED', 'Saved'))
2246
                    ->addExtraClass('btn-secondary-outline font-icon-check-mark')
2247
                    ->setAttribute('data-btn-alternate', 'btn action btn-primary font-icon-save')
2248
                    ->setUseButtonTag(true)
2249
                    ->setAttribute('data-text-alternate', _t('SilverStripe\\CMS\\Controllers\\CMSMain.SAVEDRAFT', 'Save draft'))
2250
            );
2251
        }
2252
2253
        if ($canPublish && $isOnDraft) {
2254
            // "publish", as with "save", it supports an alternate state to show when action is needed.
2255
            $majorActions->push(
2256
                $publish = FormAction::create('publish', _t(__CLASS__.'.BUTTONPUBLISHED', 'Published'))
2257
                    ->addExtraClass('btn-secondary-outline font-icon-check-mark')
2258
                    ->setAttribute('data-btn-alternate', 'btn action btn-primary font-icon-rocket')
2259
                    ->setUseButtonTag(true)
2260
                    ->setAttribute('data-text-alternate', _t(__CLASS__.'.BUTTONSAVEPUBLISH', 'Save & publish'))
2261
            );
2262
2263
            // Set up the initial state of the button to reflect the state of the underlying SiteTree object.
2264
            if ($stagesDiffer) {
2265
                $publish->addExtraClass('btn-primary font-icon-rocket');
2266
                $publish->setTitle(_t(__CLASS__.'.BUTTONSAVEPUBLISH', 'Save & publish'));
2267
                $publish->removeExtraClass('btn-secondary-outline font-icon-check-mark');
2268
            }
2269
        }
2270
2271
        $actions = new FieldList(array($majorActions, $rootTabSet));
2272
2273
        // Hook for extensions to add/remove actions.
2274
        $this->extend('updateCMSActions', $actions);
2275
2276
        return $actions;
2277
    }
2278
2279
    public function onAfterPublish()
2280
    {
2281
        // Force live sort order to match stage sort order
2282
        DB::prepared_query(
2283
            'UPDATE "SiteTree_Live"
2284
			SET "Sort" = (SELECT "SiteTree"."Sort" FROM "SiteTree" WHERE "SiteTree_Live"."ID" = "SiteTree"."ID")
2285
			WHERE EXISTS (SELECT "SiteTree"."Sort" FROM "SiteTree" WHERE "SiteTree_Live"."ID" = "SiteTree"."ID") AND "ParentID" = ?',
2286
            array($this->ParentID)
2287
        );
2288
    }
2289
2290
    /**
2291
     * Update draft dependant pages
2292
     */
2293
    public function onAfterRevertToLive()
2294
    {
2295
        // Use an alias to get the updates made by $this->publish
2296
        /** @var SiteTree $stageSelf */
2297
        $stageSelf = Versioned::get_by_stage(self::class, Versioned::DRAFT)->byID($this->ID);
2298
        $stageSelf->writeWithoutVersion();
2299
2300
        // Need to update pages linking to this one as no longer broken
2301
        foreach ($stageSelf->DependentPages() as $page) {
2302
            /** @var SiteTree $page */
2303
            $page->writeWithoutVersion();
2304
        }
2305
    }
2306
2307
    /**
2308
     * Determine if this page references a parent which is archived, and not available in stage
2309
     *
2310
     * @return bool True if there is an archived parent
2311
     */
2312
    protected function isParentArchived()
2313
    {
2314
        if ($parentID = $this->ParentID) {
2315
            /** @var SiteTree $parentPage */
2316
            $parentPage = Versioned::get_latest_version(self::class, $parentID);
2317
            if (!$parentPage || !$parentPage->isOnDraft()) {
2318
                return true;
2319
            }
2320
        }
2321
        return false;
2322
    }
2323
2324
    /**
2325
     * Restore the content in the active copy of this SiteTree page to the stage site.
2326
     *
2327
     * @return self
2328
     */
2329
    public function doRestoreToStage()
2330
    {
2331
        $this->invokeWithExtensions('onBeforeRestoreToStage', $this);
2332
2333
        // Ensure that the parent page is restored, otherwise restore to root
2334
        if ($this->isParentArchived()) {
2335
            $this->ParentID = 0;
2336
        }
2337
2338
        // if no record can be found on draft stage (meaning it has been "deleted from draft" before),
2339
        // create an empty record
2340
        if (!DB::prepared_query("SELECT \"ID\" FROM \"SiteTree\" WHERE \"ID\" = ?", array($this->ID))->value()) {
2341
            $conn = DB::get_conn();
2342
            if (method_exists($conn, 'allowPrimaryKeyEditing')) {
2343
                $conn->allowPrimaryKeyEditing(self::class, true);
2344
            }
2345
            DB::prepared_query("INSERT INTO \"SiteTree\" (\"ID\") VALUES (?)", array($this->ID));
2346
            if (method_exists($conn, 'allowPrimaryKeyEditing')) {
2347
                $conn->allowPrimaryKeyEditing(self::class, false);
2348
            }
2349
        }
2350
2351
        $oldReadingMode = Versioned::get_reading_mode();
2352
        Versioned::set_stage(Versioned::DRAFT);
2353
        $this->forceChange();
2354
        $this->write();
2355
2356
        /** @var SiteTree $result */
2357
        $result = DataObject::get_by_id(self::class, $this->ID);
2358
2359
        Versioned::set_reading_mode($oldReadingMode);
2360
2361
        // Need to update pages linking to this one as no longer broken
2362
        $this->updateDependentPages();
2363
2364
        $this->invokeWithExtensions('onAfterRestoreToStage', $this);
2365
2366
        return $result;
2367
    }
2368
2369
    /**
2370
     * Check if this page is new - that is, if it has yet to have been written to the database.
2371
     *
2372
     * @return bool
2373
     */
2374
    public function isNew()
2375
    {
2376
        /**
2377
         * This check was a problem for a self-hosted site, and may indicate a bug in the interpreter on their server,
2378
         * or a bug here. Changing the condition from empty($this->ID) to !$this->ID && !$this->record['ID'] fixed this.
2379
         */
2380
        if (empty($this->ID)) {
2381
            return true;
2382
        }
2383
2384
        if (is_numeric($this->ID)) {
2385
            return false;
2386
        }
2387
2388
        return stripos($this->ID, 'new') === 0;
2389
    }
2390
2391
    /**
2392
     * Get the class dropdown used in the CMS to change the class of a page. This returns the list of options in the
2393
     * dropdown as a Map from class name to singular name. Filters by {@link SiteTree->canCreate()}, as well as
2394
     * {@link SiteTree::$needs_permission}.
2395
     *
2396
     * @return array
2397
     */
2398
    protected function getClassDropdown()
2399
    {
2400
        $classes = self::page_type_classes();
2401
        $currentClass = null;
2402
2403
        $result = array();
2404
        foreach ($classes as $class) {
2405
            $instance = singleton($class);
2406
2407
            // if the current page type is this the same as the class type always show the page type in the list
2408
            if ($this->ClassName != $instance->ClassName) {
2409
                if ($instance instanceof HiddenClass) {
2410
                    continue;
2411
                }
2412
                if (!$instance->canCreate(null, array('Parent' => $this->ParentID ? $this->Parent() : null))) {
2413
                    continue;
2414
                }
2415
            }
2416
2417
            if ($perms = $instance->stat('need_permission')) {
2418
                if (!$this->can($perms)) {
2419
                    continue;
2420
                }
2421
            }
2422
2423
            $pageTypeName = $instance->i18n_singular_name();
2424
2425
            $currentClass = $class;
2426
            $result[$class] = $pageTypeName;
2427
2428
            // If we're in translation mode, the link between the translated pagetype title and the actual classname
2429
            // might not be obvious, so we add it in parantheses. Example: class "RedirectorPage" has the title
2430
            // "Weiterleitung" in German, so it shows up as "Weiterleitung (RedirectorPage)"
2431
            if (i18n::getData()->langFromLocale(i18n::get_locale()) != 'en') {
2432
                $result[$class] = $result[$class] .  " ({$class})";
2433
            }
2434
        }
2435
2436
        // sort alphabetically, and put current on top
2437
        asort($result);
2438
        if ($currentClass) {
2439
            $currentPageTypeName = $result[$currentClass];
2440
            unset($result[$currentClass]);
2441
            $result = array_reverse($result);
2442
            $result[$currentClass] = $currentPageTypeName;
2443
            $result = array_reverse($result);
2444
        }
2445
2446
        return $result;
2447
    }
2448
2449
    /**
2450
     * Returns an array of the class names of classes that are allowed to be children of this class.
2451
     *
2452
     * @return string[]
2453
     */
2454
    public function allowedChildren()
2455
    {
2456
        // Get config based on old FIRST_SET rules
2457
        $candidates = null;
2458
        $class = get_class($this);
2459
        while ($class) {
2460
            if (Config::inst()->exists($class, 'allowed_children', Config::UNINHERITED)) {
2461
                $candidates = Config::inst()->get($class, 'allowed_children', Config::UNINHERITED);
2462
                break;
2463
            }
2464
            $class = get_parent_class($class);
2465
        }
2466
        if (!$candidates || $candidates === 'none' || $candidates === 'SiteTree_root') {
2467
            return [];
2468
        }
2469
2470
        // Parse candidate list
2471
        $allowedChildren = [];
2472
        foreach ($candidates as $candidate) {
2473
            // If a classname is prefixed by "*", such as "*Page", then only that class is allowed - no subclasses.
2474
            // Otherwise, the class and all its subclasses are allowed.
2475
            if (substr($candidate, 0, 1) == '*') {
2476
                $allowedChildren[] = substr($candidate, 1);
2477
            } elseif ($subclasses = ClassInfo::subclassesFor($candidate)) {
2478
                foreach ($subclasses as $subclass) {
2479
                    if ($subclass == 'SiteTree_root' || singleton($subclass) instanceof HiddenClass) {
2480
                        continue;
2481
                    }
2482
                    $allowedChildren[] = $subclass;
2483
                }
2484
            }
2485
        }
2486
        return $allowedChildren;
2487
    }
2488
2489
    /**
2490
     * Returns the class name of the default class for children of this page.
2491
     *
2492
     * @return string
2493
     */
2494
    public function defaultChild()
2495
    {
2496
        $default = $this->stat('default_child');
2497
        $allowed = $this->allowedChildren();
2498
        if ($allowed) {
2499
            if (!$default || !in_array($default, $allowed)) {
2500
                $default = reset($allowed);
2501
            }
2502
            return $default;
2503
        }
2504
        return null;
2505
    }
2506
2507
    /**
2508
     * Returns the class name of the default class for the parent of this page.
2509
     *
2510
     * @return string
2511
     */
2512
    public function defaultParent()
2513
    {
2514
        return $this->stat('default_parent');
2515
    }
2516
2517
    /**
2518
     * Get the title for use in menus for this page. If the MenuTitle field is set it returns that, else it returns the
2519
     * Title field.
2520
     *
2521
     * @return string
2522
     */
2523
    public function getMenuTitle()
2524
    {
2525
        if ($value = $this->getField("MenuTitle")) {
2526
            return $value;
2527
        } else {
2528
            return $this->getField("Title");
2529
        }
2530
    }
2531
2532
2533
    /**
2534
     * Set the menu title for this page.
2535
     *
2536
     * @param string $value
2537
     */
2538
    public function setMenuTitle($value)
2539
    {
2540
        if ($value == $this->getField("Title")) {
2541
            $this->setField("MenuTitle", null);
2542
        } else {
2543
            $this->setField("MenuTitle", $value);
2544
        }
2545
    }
2546
2547
    /**
2548
     * A flag provides the user with additional data about the current page status, for example a "removed from draft"
2549
     * status. Each page can have more than one status flag. Returns a map of a unique key to a (localized) title for
2550
     * the flag. The unique key can be reused as a CSS class. Use the 'updateStatusFlags' extension point to customize
2551
     * the flags.
2552
     *
2553
     * Example (simple):
2554
     *   "deletedonlive" => "Deleted"
2555
     *
2556
     * Example (with optional title attribute):
2557
     *   "deletedonlive" => array('text' => "Deleted", 'title' => 'This page has been deleted')
2558
     *
2559
     * @param bool $cached Whether to serve the fields from cache; false regenerate them
2560
     * @return array
2561
     */
2562
    public function getStatusFlags($cached = true)
2563
    {
2564
        if (!$this->_cache_statusFlags || !$cached) {
2565
            $flags = array();
2566
            if ($this->isOnLiveOnly()) {
2567
                $flags['removedfromdraft'] = array(
2568
                    'text' => _t(__CLASS__.'.ONLIVEONLYSHORT', 'On live only'),
2569
                    'title' => _t(__CLASS__.'.ONLIVEONLYSHORTHELP', 'Page is published, but has been deleted from draft'),
2570
                );
2571
            } elseif ($this->isArchived()) {
2572
                $flags['archived'] = array(
2573
                    'text' => _t(__CLASS__.'.ARCHIVEDPAGESHORT', 'Archived'),
2574
                    'title' => _t(__CLASS__.'.ARCHIVEDPAGEHELP', 'Page is removed from draft and live'),
2575
                );
2576
            } elseif ($this->isOnDraftOnly()) {
2577
                $flags['addedtodraft'] = array(
2578
                    'text' => _t(__CLASS__.'.ADDEDTODRAFTSHORT', 'Draft'),
2579
                    'title' => _t(__CLASS__.'.ADDEDTODRAFTHELP', "Page has not been published yet")
2580
                );
2581
            } elseif ($this->isModifiedOnDraft()) {
2582
                $flags['modified'] = array(
2583
                    'text' => _t(__CLASS__.'.MODIFIEDONDRAFTSHORT', 'Modified'),
2584
                    'title' => _t(__CLASS__.'.MODIFIEDONDRAFTHELP', 'Page has unpublished changes'),
2585
                );
2586
            }
2587
2588
            $this->extend('updateStatusFlags', $flags);
2589
2590
            $this->_cache_statusFlags = $flags;
2591
        }
2592
2593
        return $this->_cache_statusFlags;
2594
    }
2595
2596
    /**
2597
     * getTreeTitle will return three <span> html DOM elements, an empty <span> with the class 'jstree-pageicon' in
2598
     * front, following by a <span> wrapping around its MenutTitle, then following by a <span> indicating its
2599
     * publication status.
2600
     *
2601
     * @return string An HTML string ready to be directly used in a template
2602
     */
2603
    public function getTreeTitle()
2604
    {
2605
        // Build the list of candidate children
2606
        $children = array();
2607
        $candidates = static::page_type_classes();
2608
        foreach ($this->allowedChildren() as $childClass) {
2609
            if (!in_array($childClass, $candidates)) {
2610
                continue;
2611
            }
2612
            $child = singleton($childClass);
2613
            if ($child->canCreate(null, array('Parent' => $this))) {
2614
                $children[$childClass] = $child->i18n_singular_name();
2615
            }
2616
        }
2617
        $flags = $this->getStatusFlags();
2618
        $treeTitle = sprintf(
2619
            "<span class=\"jstree-pageicon\"></span><span class=\"item\" data-allowedchildren=\"%s\">%s</span>",
2620
            Convert::raw2att(Convert::raw2json($children)),
2621
            Convert::raw2xml(str_replace(array("\n","\r"), "", $this->MenuTitle))
2622
        );
2623
        foreach ($flags as $class => $data) {
2624
            if (is_string($data)) {
2625
                $data = array('text' => $data);
2626
            }
2627
            $treeTitle .= sprintf(
2628
                "<span class=\"badge %s\"%s>%s</span>",
2629
                'status-' . Convert::raw2xml($class),
2630
                (isset($data['title'])) ? sprintf(' title="%s"', Convert::raw2xml($data['title'])) : '',
2631
                Convert::raw2xml($data['text'])
2632
            );
2633
        }
2634
2635
        return $treeTitle;
2636
    }
2637
2638
    /**
2639
     * Returns the page in the current page stack of the given level. Level(1) will return the main menu item that
2640
     * we're currently inside, etc.
2641
     *
2642
     * @param int $level
2643
     * @return SiteTree
2644
     */
2645
    public function Level($level)
2646
    {
2647
        $parent = $this;
2648
        $stack = array($parent);
2649
        while (($parent = $parent->Parent()) && $parent->exists()) {
2650
            array_unshift($stack, $parent);
2651
        }
2652
2653
        return isset($stack[$level-1]) ? $stack[$level-1] : null;
2654
    }
2655
2656
    /**
2657
     * Gets the depth of this page in the sitetree, where 1 is the root level
2658
     *
2659
     * @return int
2660
     */
2661
    public function getPageLevel()
2662
    {
2663
        if ($this->ParentID) {
2664
            return 1 + $this->Parent()->getPageLevel();
2665
        }
2666
        return 1;
2667
    }
2668
2669
    /**
2670
     * Find the controller name by our convention of {$ModelClass}Controller
2671
     *
2672
     * @return string
2673
     */
2674
    public function getControllerName()
2675
    {
2676
        //default controller for SiteTree objects
2677
        $controller = ContentController::class;
2678
2679
        //go through the ancestry for this class looking for
2680
        $ancestry = ClassInfo::ancestry(static::class);
2681
        // loop over the array going from the deepest descendant (ie: the current class) to SiteTree
2682
        while ($class = array_pop($ancestry)) {
2683
            //we don't need to go any deeper than the SiteTree class
2684
            if ($class == SiteTree::class) {
2685
                break;
2686
            }
2687
            // If we have a class of "{$ClassName}Controller" then we found our controller
2688
            if (class_exists($candidate = sprintf('%sController', $class))) {
2689
                $controller = $candidate;
2690
                break;
2691
            } elseif (class_exists($candidate = sprintf('%s_Controller', $class))) {
2692
                // Support the legacy underscored filename, but raise a deprecation notice
2693
                Deprecation::notice(
2694
                    '5.0',
2695
                    'Underscored controller class names are deprecated. Use "MyController" instead of "My_Controller".',
2696
                    Deprecation::SCOPE_GLOBAL
2697
                );
2698
                $controller = $candidate;
2699
                break;
2700
            }
2701
        }
2702
2703
        return $controller;
2704
    }
2705
2706
    /**
2707
     * Return the CSS classes to apply to this node in the CMS tree.
2708
     *
2709
     * @return string
2710
     */
2711
    public function CMSTreeClasses()
2712
    {
2713
        $classes = sprintf('class-%s', static::class);
2714
        if ($this->HasBrokenFile || $this->HasBrokenLink) {
2715
            $classes .= " BrokenLink";
2716
        }
2717
2718
        if (!$this->canAddChildren()) {
2719
            $classes .= " nochildren";
2720
        }
2721
2722
        if (!$this->canEdit() && !$this->canAddChildren()) {
2723
            if (!$this->canView()) {
2724
                $classes .= " disabled";
2725
            } else {
2726
                $classes .= " edit-disabled";
2727
            }
2728
        }
2729
2730
        if (!$this->ShowInMenus) {
2731
            $classes .= " notinmenu";
2732
        }
2733
2734
        return $classes;
2735
    }
2736
2737
    /**
2738
     * Stops extendCMSFields() being called on getCMSFields(). This is useful when you need access to fields added by
2739
     * subclasses of SiteTree in a extension. Call before calling parent::getCMSFields(), and reenable afterwards.
2740
     */
2741
    public static function disableCMSFieldsExtensions()
2742
    {
2743
        self::$runCMSFieldsExtensions = false;
2744
    }
2745
2746
    /**
2747
     * Reenables extendCMSFields() being called on getCMSFields() after it has been disabled by
2748
     * disableCMSFieldsExtensions().
2749
     */
2750
    public static function enableCMSFieldsExtensions()
2751
    {
2752
        self::$runCMSFieldsExtensions = true;
2753
    }
2754
2755
    public function providePermissions()
2756
    {
2757
        return array(
2758
            'SITETREE_GRANT_ACCESS' => array(
2759
                'name' => _t(__CLASS__.'.PERMISSION_GRANTACCESS_DESCRIPTION', 'Manage access rights for content'),
2760
                'help' => _t(__CLASS__.'.PERMISSION_GRANTACCESS_HELP', 'Allow setting of page-specific access restrictions in the "Pages" section.'),
2761
                'category' => _t('SilverStripe\\Security\\Permission.PERMISSIONS_CATEGORY', 'Roles and access permissions'),
2762
                'sort' => 100
2763
            ),
2764
            'SITETREE_VIEW_ALL' => array(
2765
                'name' => _t(__CLASS__.'.VIEW_ALL_DESCRIPTION', 'View any page'),
2766
                'category' => _t('SilverStripe\\Security\\Permission.CONTENT_CATEGORY', 'Content permissions'),
2767
                'sort' => -100,
2768
                'help' => _t(__CLASS__.'.VIEW_ALL_HELP', 'Ability to view any page on the site, regardless of the settings on the Access tab.  Requires the "Access to \'Pages\' section" permission')
2769
            ),
2770
            'SITETREE_EDIT_ALL' => array(
2771
                'name' => _t(__CLASS__.'.EDIT_ALL_DESCRIPTION', 'Edit any page'),
2772
                'category' => _t('SilverStripe\\Security\\Permission.CONTENT_CATEGORY', 'Content permissions'),
2773
                'sort' => -50,
2774
                'help' => _t(__CLASS__.'.EDIT_ALL_HELP', 'Ability to edit any page on the site, regardless of the settings on the Access tab.  Requires the "Access to \'Pages\' section" permission')
2775
            ),
2776
            'SITETREE_REORGANISE' => array(
2777
                'name' => _t(__CLASS__.'.REORGANISE_DESCRIPTION', 'Change site structure'),
2778
                'category' => _t('SilverStripe\\Security\\Permission.CONTENT_CATEGORY', 'Content permissions'),
2779
                'help' => _t(__CLASS__.'.REORGANISE_HELP', 'Rearrange pages in the site tree through drag&drop.'),
2780
                'sort' => 100
2781
            ),
2782
            'VIEW_DRAFT_CONTENT' => array(
2783
                'name' => _t(__CLASS__.'.VIEW_DRAFT_CONTENT', 'View draft content'),
2784
                'category' => _t('SilverStripe\\Security\\Permission.CONTENT_CATEGORY', 'Content permissions'),
2785
                'help' => _t(__CLASS__.'.VIEW_DRAFT_CONTENT_HELP', 'Applies to viewing pages outside of the CMS in draft mode. Useful for external collaborators without CMS access.'),
2786
                'sort' => 100
2787
            )
2788
        );
2789
    }
2790
2791
    /**
2792
     * Default singular name for page / sitetree
2793
     *
2794
     * @return string
2795
     */
2796 View Code Duplication
    public function singular_name()
2797
    {
2798
        $base = in_array(static::class, [Page::class, self::class]);
2799
        if ($base) {
2800
            return $this->stat('base_singular_name');
2801
        }
2802
        return parent::singular_name();
2803
    }
2804
2805
    /**
2806
     * Default plural name for page / sitetree
2807
     *
2808
     * @return string
2809
     */
2810 View Code Duplication
    public function plural_name()
2811
    {
2812
        $base = in_array(static::class, [Page::class, self::class]);
2813
        if ($base) {
2814
            return $this->stat('base_plural_name');
2815
        }
2816
        return parent::plural_name();
2817
    }
2818
2819
    /**
2820
     * Get description for this page type
2821
     *
2822
     * @return string|null
2823
     */
2824
    public function classDescription()
2825
    {
2826
        $base = in_array(static::class, [Page::class, self::class]);
2827
        if ($base) {
2828
            return $this->stat('base_description');
2829
        }
2830
        return $this->stat('description');
2831
    }
2832
2833
    /**
2834
     * Get localised description for this page
2835
     *
2836
     * @return string|null
2837
     */
2838
    public function i18n_classDescription()
2839
    {
2840
        $description = $this->classDescription();
2841
        if ($description) {
2842
            return _t(static::class.'.DESCRIPTION', $description);
2843
        }
2844
        return null;
2845
    }
2846
2847
    /**
2848
     * Overloaded to also provide entities for 'Page' class which is usually located in custom code, hence textcollector
2849
     * picks it up for the wrong folder.
2850
     *
2851
     * @return array
2852
     */
2853
    public function provideI18nEntities()
2854
    {
2855
        $entities = parent::provideI18nEntities();
2856
2857
        // Add optional description
2858
        $description = $this->classDescription();
2859
        if ($description) {
2860
            $entities[static::class . '.DESCRIPTION'] = $description;
2861
        }
2862
        return $entities;
2863
    }
2864
2865
    /**
2866
     * Returns 'root' if the current page has no parent, or 'subpage' otherwise
2867
     *
2868
     * @return string
2869
     */
2870
    public function getParentType()
2871
    {
2872
        return $this->ParentID == 0 ? 'root' : 'subpage';
2873
    }
2874
2875
    /**
2876
     * Clear the permissions cache for SiteTree
2877
     */
2878
    public static function reset()
2879
    {
2880
        $permissions = static::getPermissionChecker();
2881
        if ($permissions instanceof InheritedPermissions) {
2882
            $permissions->clearCache();
2883
        }
2884
    }
2885
2886
    /**
2887
     * Update dependant pages
2888
     */
2889
    protected function updateDependentPages()
2890
    {
2891
        // Need to flush cache to avoid outdated versionnumber references
2892
        $this->flushCache();
2893
2894
        // Need to mark pages depending to this one as broken
2895
        $dependentPages = $this->DependentPages();
2896
        if ($dependentPages) {
2897
            foreach ($dependentPages as $page) {
2898
                // $page->write() calls syncLinkTracking, which does all the hard work for us.
2899
                $page->write();
2900
            }
2901
        }
2902
    }
2903
}
2904