Completed
Push — master ( 780648...a93e4f )
by Daniel
06:19
created

CMSMain::getBreadcrumbsBackLink()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 11
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 11
rs 9.4285
c 0
b 0
f 0
cc 2
eloc 7
nc 2
nop 0
1
<?php
2
3
namespace SilverStripe\CMS\Controllers;
4
5
use InvalidArgumentException;
6
use Psr\SimpleCache\CacheInterface;
7
use SilverStripe\Admin\AdminRootController;
8
use SilverStripe\Admin\CMSBatchActionHandler;
9
use SilverStripe\Admin\LeftAndMain;
10
use SilverStripe\Admin\LeftAndMainFormRequestHandler;
11
use SilverStripe\CMS\BatchActions\CMSBatchAction_Archive;
12
use SilverStripe\CMS\BatchActions\CMSBatchAction_Publish;
13
use SilverStripe\CMS\BatchActions\CMSBatchAction_Restore;
14
use SilverStripe\CMS\BatchActions\CMSBatchAction_Unpublish;
15
use SilverStripe\CMS\Model\CurrentPageIdentifier;
16
use SilverStripe\CMS\Model\RedirectorPage;
17
use SilverStripe\CMS\Model\SiteTree;
18
use SilverStripe\CMS\Model\VirtualPage;
19
use SilverStripe\Control\Controller;
20
use SilverStripe\Control\Director;
21
use SilverStripe\Control\HTTPRequest;
22
use SilverStripe\Control\HTTPResponse;
23
use SilverStripe\Control\HTTPResponse_Exception;
24
use SilverStripe\Core\Convert;
25
use SilverStripe\Core\Environment;
26
use SilverStripe\Core\Injector\Injector;
27
use SilverStripe\Core\Manifest\ModuleLoader;
28
use SilverStripe\Forms\DateField;
29
use SilverStripe\Forms\DropdownField;
30
use SilverStripe\Forms\FieldGroup;
31
use SilverStripe\Forms\FieldList;
32
use SilverStripe\Forms\Form;
33
use SilverStripe\Forms\FormAction;
34
use SilverStripe\Forms\FormField;
35
use SilverStripe\Forms\GridField\GridField;
36
use SilverStripe\Forms\GridField\GridFieldConfig;
37
use SilverStripe\Forms\GridField\GridFieldDataColumns;
38
use SilverStripe\Forms\GridField\GridFieldLevelup;
39
use SilverStripe\Forms\GridField\GridFieldPaginator;
40
use SilverStripe\Forms\GridField\GridFieldSortableHeader;
41
use SilverStripe\Forms\HiddenField;
42
use SilverStripe\Forms\LabelField;
43
use SilverStripe\Forms\LiteralField;
44
use SilverStripe\Forms\Tab;
45
use SilverStripe\Forms\TabSet;
46
use SilverStripe\Forms\TextField;
47
use SilverStripe\ORM\ArrayList;
48
use SilverStripe\ORM\CMSPreviewable;
49
use SilverStripe\ORM\DataList;
50
use SilverStripe\ORM\DataObject;
51
use SilverStripe\ORM\DB;
52
use SilverStripe\ORM\FieldType\DBHTMLText;
53
use SilverStripe\ORM\HiddenClass;
54
use SilverStripe\ORM\Hierarchy\MarkedSet;
55
use SilverStripe\ORM\SS_List;
56
use SilverStripe\ORM\ValidationResult;
57
use SilverStripe\Security\InheritedPermissions;
58
use SilverStripe\Security\Member;
59
use SilverStripe\Security\Permission;
60
use SilverStripe\Security\PermissionProvider;
61
use SilverStripe\Security\Security;
62
use SilverStripe\Security\SecurityToken;
63
use SilverStripe\SiteConfig\SiteConfig;
64
use SilverStripe\Versioned\ChangeSet;
65
use SilverStripe\Versioned\ChangeSetItem;
66
use SilverStripe\Versioned\Versioned;
67
use SilverStripe\View\ArrayData;
68
use SilverStripe\View\Requirements;
69
use Translatable;
70
71
/**
72
 * The main "content" area of the CMS.
73
 *
74
 * This class creates a 2-frame layout - left-tree and right-form - to sit beneath the main
75
 * admin menu.
76
 *
77
 * @todo Create some base classes to contain the generic functionality that will be replicated.
78
 *
79
 * @mixin LeftAndMainPageIconsExtension
80
 */
81
class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionProvider
82
{
83
84
    private static $url_segment = 'pages';
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
85
86
    private static $url_rule = '/$Action/$ID/$OtherID';
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
87
88
    // Maintain a lower priority than other administration sections
89
    // so that Director does not think they are actions of CMSMain
90
    private static $url_priority = 39;
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
91
92
    private static $menu_title = 'Edit Page';
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
93
94
    private static $menu_icon_class = 'font-icon-sitemap';
95
96
    private static $menu_priority = 10;
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
97
98
    private static $tree_class = SiteTree::class;
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
99
100
    private static $subitem_class = Member::class;
101
102
    private static $session_namespace = self::class;
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
103
104
    private static $required_permission_codes = 'CMS_ACCESS_CMSMain';
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
105
106
    /**
107
     * Amount of results showing on a single page.
108
     *
109
     * @config
110
     * @var int
111
     */
112
    private static $page_length = 15;
113
114
    private static $allowed_actions = array(
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
115
        'archive',
116
        'deleteitems',
117
        'DeleteItemsForm',
118
        'dialog',
119
        'duplicate',
120
        'duplicatewithchildren',
121
        'publishall',
122
        'publishitems',
123
        'PublishItemsForm',
124
        'submit',
125
        'EditForm',
126
        'SearchForm',
127
        'SiteTreeAsUL',
128
        'getshowdeletedsubtree',
129
        'savetreenode',
130
        'getsubtree',
131
        'updatetreenodes',
132
        'batchactions',
133
        'treeview',
134
        'listview',
135
        'ListViewForm',
136
        'childfilter',
137
    );
138
139
    private static $url_handlers = [
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
140
        'EditForm/$ID' => 'EditForm',
141
        'listview/$ParentID' => 'listview',
142
    ];
143
144
    private static $casting = array(
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
145
        'TreeIsFiltered' => 'Boolean',
146
        'AddForm' => 'HTMLFragment',
147
        'LinkPages' => 'Text',
148
        'Link' => 'Text',
149
        'ListViewForm' => 'HTMLFragment',
150
        'ExtraTreeTools' => 'HTMLFragment',
151
        'PageList' => 'HTMLFragment',
152
        'PageListSidebar' => 'HTMLFragment',
153
        'SiteTreeHints' => 'HTMLFragment',
154
        'SecurityID' => 'Text',
155
        'SiteTreeAsUL' => 'HTMLFragment',
156
    );
157
158
    protected function init()
159
    {
160
        // set reading lang
161
        if (SiteTree::has_extension('Translatable') && !$this->getRequest()->isAjax()) {
162
            Translatable::choose_site_locale(array_keys(Translatable::get_existing_content_languages(SiteTree::class)));
163
        }
164
165
        parent::init();
166
167
        Requirements::javascript('silverstripe/cms: client/dist/js/bundle.js');
168
        Requirements::javascript('silverstripe/cms: client/dist/js/SilverStripeNavigator.js');
169
        Requirements::css('silverstripe/cms: client/dist/styles/bundle.css');
170
        Requirements::customCSS($this->generatePageIconsCss());
0 ignored issues
show
Documentation Bug introduced by
The method generatePageIconsCss does not exist on object<SilverStripe\CMS\Controllers\CMSMain>? Since you implemented __call, maybe consider adding a @method annotation.

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

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

class ParentClass {
    private $data = array();

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

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

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
171
172
        $module = ModuleLoader::getModule('silverstripe/cms');
173
        Requirements::add_i18n_javascript($module->getRelativeResourcePath('client/lang'), false, true);
174
175
        CMSBatchActionHandler::register('restore', CMSBatchAction_Restore::class);
176
        CMSBatchActionHandler::register('archive', CMSBatchAction_Archive::class);
177
        CMSBatchActionHandler::register('unpublish', CMSBatchAction_Unpublish::class);
178
        CMSBatchActionHandler::register('publish', CMSBatchAction_Publish::class);
179
    }
180
181
    public function index($request)
182
    {
183
        // In case we're not showing a specific record, explicitly remove any session state,
184
        // to avoid it being highlighted in the tree, and causing an edit form to show.
185
        if (!$request->param('Action')) {
186
            $this->setCurrentPageID(null);
187
        }
188
189
        return parent::index($request);
190
    }
191
192
    public function getResponseNegotiator()
193
    {
194
        $negotiator = parent::getResponseNegotiator();
195
196
        // ListViewForm
197
        $negotiator->setCallback('ListViewForm', function () {
198
            return $this->ListViewForm()->forTemplate();
199
        });
200
201
        return $negotiator;
202
    }
203
204
    /**
205
     * Get pages listing area
206
     *
207
     * @return DBHTMLText
208
     */
209
    public function PageList()
210
    {
211
        return $this->renderWith($this->getTemplatesWithSuffix('_PageList'));
212
    }
213
214
    /**
215
     * Page list view for edit-form
216
     *
217
     * @return DBHTMLText
218
     */
219
    public function PageListSidebar()
220
    {
221
        return $this->renderWith($this->getTemplatesWithSuffix('_PageList_Sidebar'));
222
    }
223
224
    /**
225
     * If this is set to true, the "switchView" context in the
226
     * template is shown, with links to the staging and publish site.
227
     *
228
     * @return boolean
229
     */
230
    public function ShowSwitchView()
231
    {
232
        return true;
233
    }
234
235
    /**
236
     * Overloads the LeftAndMain::ShowView. Allows to pass a page as a parameter, so we are able
237
     * to switch view also for archived versions.
238
     *
239
     * @param SiteTree $page
240
     * @return array
241
     */
242
    public function SwitchView($page = null)
243
    {
244
        if (!$page) {
245
            $page = $this->currentPage();
246
        }
247
248
        if ($page) {
249
            $nav = SilverStripeNavigator::get_for_record($page);
250
            return $nav['items'];
251
        }
252
    }
253
254
    //------------------------------------------------------------------------------------------//
255
    // Main controllers
256
257
    //------------------------------------------------------------------------------------------//
258
    // Main UI components
259
260
    /**
261
     * Override {@link LeftAndMain} Link to allow blank URL segment for CMSMain.
262
     *
263
     * @param string|null $action Action to link to.
264
     * @return string
265
     */
266
    public function Link($action = null)
267
    {
268
        $link = Controller::join_links(
269
            AdminRootController::admin_url(),
270
            $this->config()->get('url_segment'), // in case we want to change the segment
271
            '/', // trailing slash needed if $action is null!
272
            "$action"
273
        );
274
        $this->extend('updateLink', $link);
275
        return $link;
276
    }
277
278
    public function LinkPages()
279
    {
280
        return CMSPagesController::singleton()->Link();
281
    }
282
283
    public function LinkPagesWithSearch()
284
    {
285
        return $this->LinkWithSearch($this->LinkPages());
286
    }
287
288
    /**
289
     * Get link to tree view
290
     *
291
     * @return string
292
     */
293
    public function LinkTreeView()
294
    {
295
        // Tree view is just default link to main pages section (no /treeview suffix)
296
        return CMSMain::singleton()->Link();
297
    }
298
299
    /**
300
     * Get link to list view
301
     *
302
     * @return string
303
     */
304
    public function LinkListView()
305
    {
306
        // Note : Force redirect to top level page controller (no parentid)
307
        return $this->LinkWithSearch(CMSMain::singleton()->Link('listview'));
308
    }
309
310
    /**
311
     * Link to list view for children of a parent page
312
     *
313
     * @param int|string $parentID Literal parentID, or placeholder (e.g. '%d') for
314
     * client side substitution
315
     * @return string
316
     */
317
    public function LinkListViewChildren($parentID)
318
    {
319
        return $this->LinkWithSearch(Controller::join_links(
320
            CMSMain::singleton()->Link('listview'),
321
            $parentID
322
        ));
323
    }
324
325
    /**
326
     * Link to lazy-load deferred tree view
327
     *
328
     * @return string
329
     */
330
    public function LinkTreeViewDeferred()
331
    {
332
        return $this->Link('treeview');
333
    }
334
335 View Code Duplication
    public function LinkPageEdit($id = null)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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

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

Loading history...
336
    {
337
        if (!$id) {
338
            $id = $this->currentPageID();
339
        }
340
        return $this->LinkWithSearch(
341
            Controller::join_links(CMSPageEditController::singleton()->Link('show'), $id)
342
        );
343
    }
344
345 View Code Duplication
    public function LinkPageSettings()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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

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

Loading history...
346
    {
347
        if ($id = $this->currentPageID()) {
348
            return $this->LinkWithSearch(
349
                Controller::join_links(CMSPageSettingsController::singleton()->Link('show'), $id)
350
            );
351
        } else {
352
            return null;
353
        }
354
    }
355
356 View Code Duplication
    public function LinkPageHistory()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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

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

Loading history...
357
    {
358
        if ($id = $this->currentPageID()) {
359
            return $this->LinkWithSearch(
360
                Controller::join_links(CMSPageHistoryController::singleton()->Link('show'), $id)
361
            );
362
        } else {
363
            return null;
364
        }
365
    }
366
367
    /**
368
     * Return the active tab identifier for the CMS. Used by templates to decide which tab to give the active state.
369
     * The default value is "edit", as the primary content tab. Child controllers will override this.
370
     *
371
     * @return string
372
     */
373
    public function getTabIdentifier()
374
    {
375
        return 'edit';
376
    }
377
378
    public function LinkWithSearch($link)
379
    {
380
        // Whitelist to avoid side effects
381
        $params = array(
382
            'q' => (array)$this->getRequest()->getVar('q'),
383
            'ParentID' => $this->getRequest()->getVar('ParentID')
384
        );
385
        $link = Controller::join_links(
386
            $link,
387
            array_filter(array_values($params)) ? '?' . http_build_query($params) : null
388
        );
389
        $this->extend('updateLinkWithSearch', $link);
390
        return $link;
391
    }
392
393
    public function LinkPageAdd($extra = null, $placeholders = null)
394
    {
395
        $link = CMSPageAddController::singleton()->Link();
396
        $this->extend('updateLinkPageAdd', $link);
397
398
        if ($extra) {
399
            $link = Controller::join_links($link, $extra);
400
        }
401
402
        if ($placeholders) {
403
            $link .= (strpos($link, '?') === false ? "?$placeholders" : "&$placeholders");
404
        }
405
406
        return $link;
407
    }
408
409
    /**
410
     * @return string
411
     */
412
    public function LinkPreview()
413
    {
414
        $record = $this->getRecord($this->currentPageID());
415
        $baseLink = Director::absoluteBaseURL();
416
        if ($record && $record instanceof SiteTree) {
417
            // if we are an external redirector don't show a link
418
            if ($record instanceof RedirectorPage && $record->RedirectionType == 'External') {
419
                $baseLink = false;
420
            } else {
421
                $baseLink = $record->Link('?stage=Stage');
422
            }
423
        }
424
        return $baseLink;
425
    }
426
427
    /**
428
     * Return the entire site tree as a nested set of ULs
429
     */
430
    public function SiteTreeAsUL()
431
    {
432
        // Pre-cache sitetree version numbers for querying efficiency
433
        Versioned::prepopulate_versionnumber_cache(SiteTree::class, Versioned::DRAFT);
434
        Versioned::prepopulate_versionnumber_cache(SiteTree::class, Versioned::LIVE);
435
        $html = $this->getSiteTreeFor($this->config()->get('tree_class'));
436
437
        $this->extend('updateSiteTreeAsUL', $html);
438
439
        return $html;
440
    }
441
442
    /**
443
     * Get a site tree HTML listing which displays the nodes under the given criteria.
444
     *
445
     * @param string $className The class of the root object
446
     * @param string $rootID The ID of the root object.  If this is null then a complete tree will be
447
     *  shown
448
     * @param string $childrenMethod The method to call to get the children of the tree. For example,
449
     *  Children, AllChildrenIncludingDeleted, or AllHistoricalChildren
450
     * @param string $numChildrenMethod
451
     * @param callable $filterFunction
452
     * @param int $nodeCountThreshold
453
     * @return string Nested unordered list with links to each page
454
     */
455
    public function getSiteTreeFor(
456
        $className,
457
        $rootID = null,
458
        $childrenMethod = null,
459
        $numChildrenMethod = null,
460
        $filterFunction = null,
461
        $nodeCountThreshold = 30
462
    ) {
463
        // Provide better defaults from filter
464
        $filter = $this->getSearchFilter();
465
        if ($filter) {
466
            if (!$childrenMethod) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $childrenMethod of type string|null is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
467
                $childrenMethod = $filter->getChildrenMethod();
468
            }
469
            if (!$numChildrenMethod) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $numChildrenMethod of type string|null is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
470
                $numChildrenMethod = $filter->getNumChildrenMethod();
471
            }
472
            if (!$filterFunction) {
473
                $filterFunction = function ($node) use ($filter) {
474
                    return $filter->isPageIncluded($node);
475
                };
476
            }
477
        }
478
479
        // Build set from node and begin marking
480
        $record = ($rootID) ? $this->getRecord($rootID) : null;
481
        $rootNode = $record ? $record : DataObject::singleton($className);
482
        $markingSet = MarkedSet::create($rootNode, $childrenMethod, $numChildrenMethod, $nodeCountThreshold);
483
484
        // Set filter function
485
        if ($filterFunction) {
486
            $markingSet->setMarkingFilterFunction($filterFunction);
487
        }
488
489
        // Mark tree from this node
490
        $markingSet->markPartialTree();
491
492
        // Ensure current page is exposed
493
        $currentPage = $this->currentPage();
494
        if ($currentPage) {
495
            $markingSet->markToExpose($currentPage);
496
        }
497
498
        // Pre-cache permissions
499
        $checker = SiteTree::getPermissionChecker();
500
        if ($checker instanceof InheritedPermissions) {
501
            $checker->prePopulatePermissionCache(
502
                InheritedPermissions::EDIT,
503
                $markingSet->markedNodeIDs()
504
            );
505
        }
506
507
        // Render using full-subtree template
508
        return $markingSet->renderChildren(
509
            [ self::class . '_SubTree', 'type' => 'Includes' ],
0 ignored issues
show
Documentation introduced by
array(self::class . '_Su..., 'type' => 'Includes') is of type array<integer|string,str...ring","type":"string"}>, but the function expects a string|null.

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
510
            $this->getTreeNodeCustomisations()
511
        );
512
    }
513
514
515
    /**
516
     * Get callback to determine template customisations for nodes
517
     *
518
     * @return callable
519
     */
520
    protected function getTreeNodeCustomisations()
521
    {
522
        $rootTitle = $this->getCMSTreeTitle();
523
        return function (SiteTree $node) use ($rootTitle) {
524
            return [
525
                'listViewLink' => $this->LinkListViewChildren($node->ID),
526
                'rootTitle' => $rootTitle,
527
                'extraClass' => $this->getTreeNodeClasses($node),
528
            ];
529
        };
530
    }
531
532
    /**
533
     * Get extra CSS classes for a page's tree node
534
     *
535
     * @param SiteTree $node
536
     * @return string
537
     */
538
    public function getTreeNodeClasses(SiteTree $node)
539
    {
540
        // Get classes from object
541
        $classes = $node->CMSTreeClasses();
542
543
        // Get status flag classes
544
        $flags = $node->getStatusFlags();
545
        if ($flags) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $flags of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
546
            $statuses = array_keys($flags);
547
            foreach ($statuses as $s) {
548
                $classes .= ' status-' . $s;
549
            }
550
        }
551
552
        // Get additional filter classes
553
        $filter = $this->getSearchFilter();
554
        if ($filter && ($filterClasses = $filter->getPageClasses($node))) {
555
            if (is_array($filterClasses)) {
556
                $filterClasses = implode(' ', $filterClasses);
557
            }
558
            $classes .= ' ' . $filterClasses;
559
        }
560
561
        return trim($classes);
562
    }
563
564
    /**
565
     * Get a subtree underneath the request param 'ID'.
566
     * If ID = 0, then get the whole tree.
567
     *
568
     * @param HTTPRequest $request
569
     * @return string
570
     */
571
    public function getsubtree($request)
572
    {
573
        $html = $this->getSiteTreeFor(
574
            $this->config()->get('tree_class'),
575
            $request->getVar('ID'),
576
            null,
577
            null,
578
            null,
579
            $request->getVar('minNodeCount')
580
        );
581
582
        // Trim off the outer tag
583
        $html = preg_replace('/^[\s\t\r\n]*<ul[^>]*>/', '', $html);
584
        $html = preg_replace('/<\/ul[^>]*>[\s\t\r\n]*$/', '', $html);
585
586
        return $html;
587
    }
588
589
    /**
590
     * Allows requesting a view update on specific tree nodes.
591
     * Similar to {@link getsubtree()}, but doesn't enforce loading
592
     * all children with the node. Useful to refresh views after
593
     * state modifications, e.g. saving a form.
594
     *
595
     * @param HTTPRequest $request
596
     * @return HTTPResponse
597
     */
598
    public function updatetreenodes($request)
599
    {
600
        $data = array();
601
        $ids = explode(',', $request->getVar('ids'));
602
        foreach ($ids as $id) {
603
            if ($id === "") {
604
                continue; // $id may be a blank string, which is invalid and should be skipped over
605
            }
606
607
            $record = $this->getRecord($id);
608
            if (!$record) {
609
                continue; // In case a page is no longer available
610
            }
611
612
            // Create marking set with sole marked root
613
            $markingSet = MarkedSet::create($record);
614
            $markingSet->setMarkingFilterFunction(function () {
615
                return false;
616
            });
617
            $markingSet->markUnexpanded($record);
618
619
            // Find the next & previous nodes, for proper positioning (Sort isn't good enough - it's not a raw offset)
620
            // TODO: These methods should really be in hierarchy - for a start it assumes Sort exists
621
            $prev = null;
622
623
            $className = $this->config()->get('tree_class');
624
            $next = DataObject::get($className)
625
                ->filter('ParentID', $record->ParentID)
626
                ->filter('Sort:GreaterThan', $record->Sort)
627
                ->first();
628
629
            if (!$next) {
630
                $prev = DataObject::get($className)
631
                    ->filter('ParentID', $record->ParentID)
632
                    ->filter('Sort:LessThan', $record->Sort)
633
                    ->reverse()
634
                    ->first();
635
            }
636
637
            // Render using single node template
638
            $html = $markingSet->renderChildren(
639
                [ self::class . '_TreeNode', 'type' => 'Includes'],
0 ignored issues
show
Documentation introduced by
array(self::class . '_Tr..., 'type' => 'Includes') is of type array<integer|string,str...ring","type":"string"}>, but the function expects a string|null.

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
640
                $this->getTreeNodeCustomisations()
641
            );
642
643
            $data[$id] = array(
644
                'html' => $html,
645
                'ParentID' => $record->ParentID,
646
                'NextID' => $next ? $next->ID : null,
647
                'PrevID' => $prev ? $prev->ID : null
648
            );
649
        }
650
        return $this
651
            ->getResponse()
652
            ->addHeader('Content-Type', 'application/json')
653
            ->setBody(Convert::raw2json($data));
654
    }
655
656
    /**
657
     * Update the position and parent of a tree node.
658
     * Only saves the node if changes were made.
659
     *
660
     * Required data:
661
     * - 'ID': The moved node
662
     * - 'ParentID': New parent relation of the moved node (0 for root)
663
     * - 'SiblingIDs': Array of all sibling nodes to the moved node (incl. the node itself).
664
     *   In case of a 'ParentID' change, relates to the new siblings under the new parent.
665
     *
666
     * @param HTTPRequest $request
667
     * @return HTTPResponse JSON string with a
668
     * @throws HTTPResponse_Exception
669
     */
670
    public function savetreenode($request)
671
    {
672
        if (!SecurityToken::inst()->checkRequest($request)) {
673
            return $this->httpError(400);
674
        }
675
        if (!Permission::check('SITETREE_REORGANISE') && !Permission::check('ADMIN')) {
676
            return $this->httpError(
677
                403,
678
                _t(
679
                    __CLASS__.'.CANT_REORGANISE',
680
                    "You do not have permission to rearange the site tree. Your change was not saved."
681
                )
682
            );
683
        }
684
685
        $className = $this->config()->get('tree_class');
686
        $id = $request->requestVar('ID');
687
        $parentID = $request->requestVar('ParentID');
688
        if (!is_numeric($id) || !is_numeric($parentID)) {
689
            return $this->httpError(400);
690
        }
691
692
        // Check record exists in the DB
693
        /** @var SiteTree $node */
694
        $node = DataObject::get_by_id($className, $id);
695
        if (!$node) {
696
            return $this->httpError(
697
                500,
698
                _t(
699
                    __CLASS__.'.PLEASESAVE',
700
                    "Please Save Page: This page could not be updated because it hasn't been saved yet."
701
                )
702
            );
703
        }
704
705
        // Check top level permissions
706
        $root = $node->getParentType();
707
        if (($parentID == '0' || $root == 'root') && !SiteConfig::current_site_config()->canCreateTopLevel()) {
708
            return $this->httpError(
709
                403,
710
                _t(
711
                    __CLASS__.'.CANT_REORGANISE',
712
                    "You do not have permission to alter Top level pages. Your change was not saved."
713
                )
714
            );
715
        }
716
717
        $siblingIDs = $request->requestVar('SiblingIDs');
718
        $statusUpdates = array('modified'=>array());
719
720
        if (!$node->canEdit()) {
721
            return Security::permissionFailure($this);
722
        }
723
724
        // Update hierarchy (only if ParentID changed)
725
        if ($node->ParentID != $parentID) {
0 ignored issues
show
Documentation introduced by
The property ParentID does not exist on object<SilverStripe\CMS\Model\SiteTree>. Since you implemented __get, maybe consider adding a @property annotation.

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

<?php

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

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

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

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

}

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

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

See also the PhpDoc documentation for @property.

Loading history...
726
            $node->ParentID = (int)$parentID;
0 ignored issues
show
Documentation introduced by
The property ParentID does not exist on object<SilverStripe\CMS\Model\SiteTree>. Since you implemented __set, maybe consider adding a @property annotation.

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

<?php

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

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

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

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

}

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

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

See also the PhpDoc documentation for @property.

Loading history...
727
            $node->write();
728
729
            $statusUpdates['modified'][$node->ID] = array(
730
                'TreeTitle' => $node->TreeTitle
0 ignored issues
show
Documentation introduced by
The property TreeTitle does not exist on object<SilverStripe\CMS\Model\SiteTree>. Since you implemented __get, maybe consider adding a @property annotation.

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

<?php

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

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

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

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

}

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

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

See also the PhpDoc documentation for @property.

Loading history...
731
            );
732
733
            // Update all dependent pages
734
            $virtualPages = VirtualPage::get()->filter("CopyContentFromID", $node->ID);
735
            foreach ($virtualPages as $virtualPage) {
736
                $statusUpdates['modified'][$virtualPage->ID] = array(
737
                    'TreeTitle' => $virtualPage->TreeTitle()
738
                );
739
            }
740
741
            $this->getResponse()->addHeader(
742
                'X-Status',
743
                rawurlencode(_t(__CLASS__.'.REORGANISATIONSUCCESSFUL', 'Reorganised the site tree successfully.'))
744
            );
745
        }
746
747
        // Update sorting
748
        if (is_array($siblingIDs)) {
749
            $counter = 0;
750
            foreach ($siblingIDs as $id) {
751
                if ($id == $node->ID) {
752
                    $node->Sort = ++$counter;
753
                    $node->write();
754
                    $statusUpdates['modified'][$node->ID] = array(
755
                        'TreeTitle' => $node->TreeTitle
0 ignored issues
show
Documentation introduced by
The property TreeTitle does not exist on object<SilverStripe\CMS\Model\SiteTree>. Since you implemented __get, maybe consider adding a @property annotation.

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

<?php

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

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

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

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

}

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

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

See also the PhpDoc documentation for @property.

Loading history...
756
                    );
757
                } elseif (is_numeric($id)) {
758
                    // Nodes that weren't "actually moved" shouldn't be registered as
759
                    // having been edited; do a direct SQL update instead
760
                    ++$counter;
761
                    $table = DataObject::getSchema()->baseDataTable($className);
762
                    DB::prepared_query(
763
                        "UPDATE \"$table\" SET \"Sort\" = ? WHERE \"ID\" = ?",
764
                        array($counter, $id)
765
                    );
766
                }
767
            }
768
769
            $this->getResponse()->addHeader(
770
                'X-Status',
771
                rawurlencode(_t(__CLASS__.'.REORGANISATIONSUCCESSFUL', 'Reorganised the site tree successfully.'))
772
            );
773
        }
774
775
        return $this
776
            ->getResponse()
777
            ->addHeader('Content-Type', 'application/json')
778
            ->setBody(Convert::raw2json($statusUpdates));
779
    }
780
781
    public function CanOrganiseSitetree()
782
    {
783
        return !Permission::check('SITETREE_REORGANISE') && !Permission::check('ADMIN') ? false : true;
784
    }
785
786
    /**
787
     * @return boolean
788
     */
789
    public function TreeIsFiltered()
790
    {
791
        $query = $this->getRequest()->getVar('q');
792
793
        if (!$query || (count($query) === 1 && isset($query['FilterClass']) && $query['FilterClass'] === 'SilverStripe\\CMS\\Controllers\\CMSSiteTreeFilter_Search')) {
794
            return false;
795
        }
796
797
        return true;
798
    }
799
800
    public function ExtraTreeTools()
801
    {
802
        $html = '';
803
        $this->extend('updateExtraTreeTools', $html);
804
        return $html;
805
    }
806
807
    /**
808
     * Returns a Form for page searching for use in templates.
809
     *
810
     * Can be modified from a decorator by a 'updateSearchForm' method
811
     *
812
     * @return Form
813
     */
814
    public function SearchForm()
815
    {
816
        // Create the fields
817
        $content = new TextField('q[Term]', _t('SilverStripe\\CMS\\Search\\SearchForm.FILTERLABELTEXT', 'Search'));
818
        $dateFrom = new DateField(
819
            'q[LastEditedFrom]',
820
            _t('SilverStripe\\CMS\\Search\\SearchForm.FILTERDATEFROM', 'From')
821
        );
822
        $dateTo = new DateField(
823
            'q[LastEditedTo]',
824
            _t('SilverStripe\\CMS\\Search\\SearchForm.FILTERDATETO', 'To')
825
        );
826
        $pageFilter = new DropdownField(
827
            'q[FilterClass]',
828
            _t('SilverStripe\\CMS\\Controllers\\CMSMain.PAGES', 'Page status'),
829
            CMSSiteTreeFilter::get_all_filters()
830
        );
831
        $pageClasses = new DropdownField(
832
            'q[ClassName]',
833
            _t('SilverStripe\\CMS\\Controllers\\CMSMain.PAGETYPEOPT', 'Page type', 'Dropdown for limiting search to a page type'),
834
            $this->getPageTypes()
835
        );
836
        $pageClasses->setEmptyString(_t('SilverStripe\\CMS\\Controllers\\CMSMain.PAGETYPEANYOPT', 'Any'));
837
838
        // Group the Datefields
839
        $dateGroup = new FieldGroup(
840
            $dateFrom,
841
            $dateTo
842
        );
843
        $dateGroup->setTitle(_t('SilverStripe\\CMS\\Search\\SearchForm.PAGEFILTERDATEHEADING', 'Last edited'));
844
845
        // view mode
846
        $viewMode = HiddenField::create('view', false, $this->ViewState('listview'));
847
848
        // Create the Field list
849
        $fields = new FieldList(
850
            $content,
851
            $pageFilter,
852
            $pageClasses,
853
            $dateGroup,
854
            $viewMode
855
        );
856
857
        // Create the Search and Reset action
858
        $actions = new FieldList(
859
            FormAction::create('doSearch', _t('SilverStripe\\CMS\\Controllers\\CMSMain.APPLY_FILTER', 'Search'))
860
                ->addExtraClass('btn btn-primary'),
861
            FormAction::create('clear', _t('SilverStripe\\CMS\\Controllers\\CMSMain.CLEAR_FILTER', 'Clear'))
862
                ->setAttribute('type', 'reset')
863
                ->addExtraClass('btn btn-secondary')
864
        );
865
866
        // Use <button> to allow full jQuery UI styling on the all of the Actions
867
        /** @var FormAction $action */
868
        foreach ($actions->dataFields() as $action) {
869
            /** @var FormAction $action */
870
            $action->setUseButtonTag(true);
871
        }
872
873
        // Create the form
874
        /** @skipUpgrade */
875
        $form = Form::create($this, 'SearchForm', $fields, $actions)
876
            ->addExtraClass('cms-search-form')
877
            ->setFormMethod('GET')
878
            ->setFormAction(CMSMain::singleton()->Link())
879
            ->disableSecurityToken()
880
            ->unsetValidator();
881
882
        // Load the form with previously sent search data
883
        $form->loadDataFrom($this->getRequest()->getVars());
884
885
        // Allow decorators to modify the form
886
        $this->extend('updateSearchForm', $form);
887
888
        return $form;
889
    }
890
891
    /**
892
     * Returns a sorted array suitable for a dropdown with pagetypes and their translated name
893
     *
894
     * @return array
895
     */
896
    protected function getPageTypes()
897
    {
898
        $pageTypes = array();
899
        foreach (SiteTree::page_type_classes() as $pageTypeClass) {
900
            $pageTypes[$pageTypeClass] = SiteTree::singleton($pageTypeClass)->i18n_singular_name();
901
        }
902
        asort($pageTypes);
903
        return $pageTypes;
904
    }
905
906
    public function doSearch($data, $form)
0 ignored issues
show
Unused Code introduced by
The parameter $data is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $form is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
907
    {
908
        return $this->getsubtree($this->getRequest());
909
    }
910
911
    /**
912
     * Get "back" url for breadcrumbs
913
     *
914
     * @return string
915
     */
916
    public function getBreadcrumbsBackLink()
917
    {
918
        $breadcrumbs = $this->Breadcrumbs();
919
        if ($breadcrumbs->count() < 2) {
920
            return $this->LinkPages();
921
        }
922
        // Get second from end breadcrumb
923
        return $breadcrumbs
924
            ->offsetGet($breadcrumbs->count() - 2)
925
            ->Link;
926
    }
927
928
    /**
929
     * @param bool $unlinked
930
     * @return ArrayList
931
     */
932
    public function Breadcrumbs($unlinked = false)
933
    {
934
        $items = new ArrayList();
935
936
        // Check if we are editing a page
937
        /** @var SiteTree $record */
938
        $record = $this->currentPage();
939
        if (!$record) {
940
            $items->push(new ArrayData(array(
941
                'Title' => CMSPagesController::menu_title(),
942
                'Link' => ($unlinked) ? false : $this->LinkPages()
943
            )));
944
            return $items;
945
        }
946
947
        // Add all ancestors
948
        $ancestors = $record->getAncestors();
0 ignored issues
show
Documentation Bug introduced by
The method getAncestors does not exist on object<SilverStripe\CMS\Model\SiteTree>? Since you implemented __call, maybe consider adding a @method annotation.

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

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

class ParentClass {
    private $data = array();

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

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

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
949
        $ancestors = new ArrayList(array_reverse($ancestors->toArray()));
950
        $ancestors->push($record);
951
        /** @var SiteTree $ancestor */
952
        foreach ($ancestors as $ancestor) {
953
            $items->push(new ArrayData(array(
954
                'Title' => $ancestor->getMenuTitle(),
955
                'Link' => ($unlinked)
956
                    ? false
957
                    : $ancestor->CMSEditLink()
958
            )));
959
        }
960
961
        return $items;
962
    }
963
964
    /**
965
     * Create serialized JSON string with site tree hints data to be injected into
966
     * 'data-hints' attribute of root node of jsTree.
967
     *
968
     * @return string Serialized JSON
969
     */
970
    public function SiteTreeHints()
971
    {
972
        $classes = SiteTree::page_type_classes();
973
974
        $cacheCanCreate = array();
975
        foreach ($classes as $class) {
976
            $cacheCanCreate[$class] = singleton($class)->canCreate();
977
        }
978
979
        // Generate basic cache key. Too complex to encompass all variations
980
        $cache = Injector::inst()->get(CacheInterface::class . '.CMSMain_SiteTreeHints');
981
        $cacheKey = md5(implode('_', array(Security::getCurrentUser()->ID, implode(',', $cacheCanCreate), implode(',', $classes))));
982
        if ($this->getRequest()->getVar('flush')) {
983
            $cache->clear();
984
        }
985
        $json = $cache->get($cacheKey);
986
        if (!$json) {
987
            $def['Root'] = array();
0 ignored issues
show
Coding Style Comprehensibility introduced by
$def was never initialized. Although not strictly required by PHP, it is generally a good practice to add $def = array(); before regardless.

Adding an explicit array definition is generally preferable to implicit array definition as it guarantees a stable state of the code.

Let’s take a look at an example:

foreach ($collection as $item) {
    $myArray['foo'] = $item->getFoo();

    if ($item->hasBar()) {
        $myArray['bar'] = $item->getBar();
    }

    // do something with $myArray
}

As you can see in this example, the array $myArray is initialized the first time when the foreach loop is entered. You can also see that the value of the bar key is only written conditionally; thus, its value might result from a previous iteration.

This might or might not be intended. To make your intention clear, your code more readible and to avoid accidental bugs, we recommend to add an explicit initialization $myArray = array() either outside or inside the foreach loop.

Loading history...
988
            $def['Root']['disallowedChildren'] = array();
989
990
            // Contains all possible classes to support UI controls listing them all,
991
            // such as the "add page here" context menu.
992
            $def['All'] = array();
993
994
            // Identify disallows and set globals
995
            foreach ($classes as $class) {
996
                $obj = singleton($class);
997
                if ($obj instanceof HiddenClass) {
998
                    continue;
999
                }
1000
1001
                // Name item
1002
                $def['All'][$class] = array(
1003
                    'title' => $obj->i18n_singular_name()
1004
                );
1005
1006
                // Check if can be created at the root
1007
                $needsPerm = $obj->config()->get('need_permission');
1008
                if (!$obj->config()->get('can_be_root')
1009
                    || (!array_key_exists($class, $cacheCanCreate) || !$cacheCanCreate[$class])
1010
                    || ($needsPerm && !$this->can($needsPerm))
1011
                ) {
1012
                    $def['Root']['disallowedChildren'][] = $class;
1013
                }
1014
1015
                // Hint data specific to the class
1016
                $def[$class] = array();
1017
1018
                $defaultChild = $obj->defaultChild();
1019
                if ($defaultChild !== 'Page' && $defaultChild !== null) {
1020
                    $def[$class]['defaultChild'] = $defaultChild;
1021
                }
1022
1023
                $defaultParent = $obj->defaultParent();
1024
                if ($defaultParent !== 1 && $defaultParent !== null) {
1025
                    $def[$class]['defaultParent'] = $defaultParent;
1026
                }
1027
            }
1028
1029
            $this->extend('updateSiteTreeHints', $def);
1030
1031
            $json = Convert::raw2json($def);
1032
            $cache->set($cacheKey, $json);
1033
        }
1034
        return $json;
1035
    }
1036
1037
    /**
1038
     * Populates an array of classes in the CMS
1039
     * which allows the user to change the page type.
1040
     *
1041
     * @return SS_List
1042
     */
1043
    public function PageTypes()
1044
    {
1045
        $classes = SiteTree::page_type_classes();
1046
1047
        $result = new ArrayList();
1048
1049
        foreach ($classes as $class) {
1050
            $instance = SiteTree::singleton($class);
1051
            if ($instance instanceof HiddenClass) {
1052
                continue;
1053
            }
1054
1055
            // skip this type if it is restricted
1056
            if ($instance->config()->get('need_permission') && !$this->can(singleton($class)->config()->get('need_permission'))) {
1057
                continue;
1058
            }
1059
1060
            $singularName = $instance->i18n_singular_name();
1061
            $description = $instance->i18n_classDescription();
1062
1063
            $result->push(new ArrayData(array(
1064
                'ClassName' => $class,
1065
                'AddAction' => $singularName,
1066
                'Description' => $description,
1067
                // TODO Sprite support
1068
                'IconURL' => $instance->config()->get('icon'),
1069
                'Title' => $singularName,
1070
            )));
1071
        }
1072
1073
        $result = $result->sort('AddAction');
1074
1075
        return $result;
1076
    }
1077
1078
    /**
1079
     * Get a database record to be managed by the CMS.
1080
     *
1081
     * @param int $id Record ID
1082
     * @param int $versionID optional Version id of the given record
1083
     * @return SiteTree
1084
     */
1085
    public function getRecord($id, $versionID = null)
1086
    {
1087
        if (!$id) {
1088
            return null;
1089
        }
1090
        $treeClass = $this->config()->get('tree_class');
1091
        if ($id instanceof $treeClass) {
1092
            return $id;
1093
        }
1094
        if (substr($id, 0, 3) == 'new') {
1095
            return $this->getNewItem($id);
1096
        }
1097
        if (!is_numeric($id)) {
1098
            return null;
1099
        }
1100
1101
        $currentStage = Versioned::get_reading_mode();
1102
1103
        if ($this->getRequest()->getVar('Version')) {
1104
            $versionID = (int) $this->getRequest()->getVar('Version');
1105
        }
1106
1107
        /** @var SiteTree $record */
1108
        if ($versionID) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $versionID of type integer|null is loosely compared to true; this is ambiguous if the integer can be zero. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
1109
            $record = Versioned::get_version($treeClass, $id, $versionID);
1110
        } else {
1111
            $record = DataObject::get_by_id($treeClass, $id);
1112
        }
1113
1114
        // Then, try getting a record from the live site
1115
        if (!$record) {
1116
            // $record = Versioned::get_one_by_stage($treeClass, "Live", "\"$treeClass\".\"ID\" = $id");
1117
            Versioned::set_stage(Versioned::LIVE);
1118
            singleton($treeClass)->flushCache();
1119
1120
            $record = DataObject::get_by_id($treeClass, $id);
1121
        }
1122
1123
        // Then, try getting a deleted record
1124
        if (!$record) {
1125
            $record = Versioned::get_latest_version($treeClass, $id);
1126
        }
1127
1128
        // Set the reading mode back to what it was.
1129
        Versioned::set_reading_mode($currentStage);
1130
1131
        return $record;
1132
    }
1133
1134
    /**
1135
     * {@inheritdoc}
1136
     *
1137
     * @param HTTPRequest $request
1138
     * @return Form
1139
     */
1140
    public function EditForm($request = null)
1141
    {
1142
        // set page ID from request
1143
        if ($request) {
1144
            // Validate id is present
1145
            $id = $request->param('ID');
1146
            if (!isset($id)) {
1147
                $this->httpError(400);
1148
                return null;
1149
            }
1150
            $this->setCurrentPageID($id);
1151
        }
1152
        return $this->getEditForm();
1153
    }
1154
1155
    /**
1156
     * @param int $id
1157
     * @param FieldList $fields
1158
     * @return Form
1159
     */
1160
    public function getEditForm($id = null, $fields = null)
1161
    {
1162
        // Get record
1163
        if (!$id) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $id of type integer|null is loosely compared to false; this is ambiguous if the integer can be zero. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
1164
            $id = $this->currentPageID();
1165
        }
1166
        /** @var SiteTree $record */
1167
        $record = $this->getRecord($id);
1168
1169
        // Check parent form can be generated
1170
        $form = parent::getEditForm($record, $fields);
0 ignored issues
show
Documentation introduced by
$record is of type object<SilverStripe\CMS\Model\SiteTree>, but the function expects a integer|null.

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1171
        if (!$form || !$record) {
1172
            return $form;
1173
        }
1174
1175
        if (!$fields) {
1176
            $fields = $form->Fields();
1177
        }
1178
1179
        // Add extra fields
1180
        $deletedFromStage = !$record->isOnDraft();
0 ignored issues
show
Documentation Bug introduced by
The method isOnDraft does not exist on object<SilverStripe\CMS\Model\SiteTree>? Since you implemented __call, maybe consider adding a @method annotation.

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

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

class ParentClass {
    private $data = array();

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

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

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
1181
        $fields->push($idField = new HiddenField("ID", false, $id));
0 ignored issues
show
Documentation introduced by
false is of type boolean, but the function expects a null|string.

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1182
        // Necessary for different subsites
1183
        $fields->push($liveLinkField = new HiddenField("AbsoluteLink", false, $record->AbsoluteLink()));
0 ignored issues
show
Documentation introduced by
false is of type boolean, but the function expects a null|string.

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1184
        $fields->push($liveLinkField = new HiddenField("LiveLink"));
1185
        $fields->push($stageLinkField = new HiddenField("StageLink"));
1186
        $fields->push($archiveWarningMsgField = new HiddenField("ArchiveWarningMessage"));
1187
        $fields->push(new HiddenField("TreeTitle", false, $record->getTreeTitle()));
0 ignored issues
show
Documentation introduced by
false is of type boolean, but the function expects a null|string.

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1188
1189
        $archiveWarningMsgField->setValue($this->getArchiveWarningMessage($record));
1190
1191
        // Build preview / live links
1192
        $liveLink = $record->getAbsoluteLiveLink();
1193
        if ($liveLink) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $liveLink of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
1194
            $liveLinkField->setValue($liveLink);
1195
        }
1196
        if (!$deletedFromStage) {
1197
            $stageLink = Controller::join_links($record->AbsoluteLink(), '?stage=Stage');
1198
            if ($stageLink) {
1199
                $stageLinkField->setValue($stageLink);
1200
            }
1201
        }
1202
1203
        // Added in-line to the form, but plucked into different view by LeftAndMain.Preview.js upon load
1204
        /** @skipUpgrade */
1205
        if ($record instanceof CMSPreviewable && !$fields->fieldByName('SilverStripeNavigator')) {
1206
            $navField = new LiteralField('SilverStripeNavigator', $this->getSilverStripeNavigator());
1207
            $navField->setAllowHTML(true);
1208
            $fields->push($navField);
1209
        }
1210
1211
        // getAllCMSActions can be used to completely redefine the action list
1212
        if ($record->hasMethod('getAllCMSActions')) {
1213
            $actions = $record->getAllCMSActions();
0 ignored issues
show
Documentation Bug introduced by
The method getAllCMSActions does not exist on object<SilverStripe\CMS\Model\SiteTree>? Since you implemented __call, maybe consider adding a @method annotation.

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

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

class ParentClass {
    private $data = array();

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

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

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
1214
        } else {
1215
            $actions = $record->getCMSActions();
1216
1217
            // Find and remove action menus that have no actions.
1218
            if ($actions && $actions->count()) {
1219
                /** @var TabSet $tabset */
1220
                $tabset = $actions->fieldByName('ActionMenus');
1221
                if ($tabset) {
1222
                    /** @var Tab $tab */
1223
                    foreach ($tabset->getChildren() as $tab) {
1224
                        if (!$tab->getChildren()->count()) {
1225
                            $tabset->removeByName($tab->getName());
1226
                        }
1227
                    }
1228
                }
1229
            }
1230
        }
1231
1232
        // Use <button> to allow full jQuery UI styling
1233
        $actionsFlattened = $actions->dataFields();
1234
        if ($actionsFlattened) {
1235
            /** @var FormAction $action */
1236
            foreach ($actionsFlattened as $action) {
1237
                $action->setUseButtonTag(true);
1238
            }
1239
        }
1240
1241
        // TODO Can't merge $FormAttributes in template at the moment
1242
        $form->addExtraClass('center ' . $this->BaseCSSClasses());
1243
        // Set validation exemptions for specific actions
1244
        $form->setValidationExemptActions(array(
1245
            'restore',
1246
            'revert',
1247
            'deletefromlive',
1248
            'delete',
1249
            'unpublish',
1250
            'rollback',
1251
            'doRollback',
1252
            'archive',
1253
        ));
1254
1255
        // Announce the capability so the frontend can decide whether to allow preview or not.
1256
        if ($record instanceof CMSPreviewable) {
1257
            $form->addExtraClass('cms-previewable');
1258
        }
1259
        $form->addExtraClass('fill-height flexbox-area-grow');
1260
1261
        if (!$record->canEdit() || $deletedFromStage) {
1262
            $readonlyFields = $form->Fields()->makeReadonly();
1263
            $form->setFields($readonlyFields);
1264
        }
1265
1266
        $form->Fields()->setForm($form);
1267
1268
        $this->extend('updateEditForm', $form);
1269
1270
        // Use custom reqest handler for LeftAndMain requests;
1271
        // CMS Forms cannot be identified solely by name, but also need ID (and sometimes OtherID)
1272
        $form->setRequestHandler(
1273
            LeftAndMainFormRequestHandler::create($form, [$id])
1274
        );
1275
        return $form;
1276
    }
1277
1278
    public function EmptyForm()
1279
    {
1280
        $fields = new FieldList(
1281
            new LabelField('PageDoesntExistLabel', _t('SilverStripe\\CMS\\Controllers\\CMSMain.PAGENOTEXISTS', "This page doesn't exist"))
1282
        );
1283
        $form = parent::EmptyForm();
1284
        $form->setFields($fields);
1285
        $fields->setForm($form);
1286
        return $form;
1287
    }
1288
1289
    /**
1290
     * Build an archive warning message based on the page's children
1291
     *
1292
     * @param SiteTree $record
1293
     * @return string
1294
     */
1295
    protected function getArchiveWarningMessage($record)
1296
    {
1297
        // Get all page's descendants
1298
        $record->collateDescendants(true, $descendants);
0 ignored issues
show
Documentation introduced by
true is of type boolean, but the function expects a string.

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1299
        if (!$descendants) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $descendants of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
1300
            $descendants = [];
1301
        }
1302
1303
        // Get all campaigns that the page and its descendants belong to
1304
        $inChangeSetIDs = ChangeSetItem::get_for_object($record)->column('ChangeSetID');
1305
1306
        foreach ($descendants as $page) {
1307
            $inChangeSetIDs = array_merge($inChangeSetIDs, ChangeSetItem::get_for_object($page)->column('ChangeSetID'));
1308
        }
1309
1310
        if (count($inChangeSetIDs) > 0) {
1311
            $inChangeSets = ChangeSet::get()->filter(['ID' => $inChangeSetIDs, 'State' => ChangeSet::STATE_OPEN]);
1312
        } else {
1313
            $inChangeSets = new ArrayList();
1314
        }
1315
1316
        $numCampaigns = ChangeSet::singleton()->i18n_pluralise($inChangeSets->count());
1317
        $numCampaigns = mb_strtolower($numCampaigns);
1318
1319
        if (count($descendants) > 0 && $inChangeSets->count() > 0) {
1320
            $archiveWarningMsg = _t('SilverStripe\\CMS\\Controllers\\CMSMain.ArchiveWarningWithChildrenAndCampaigns', 'Warning: This page and all of its child pages will be unpublished and automatically removed from their associated {NumCampaigns} before being sent to the archive.\n\nAre you sure you want to proceed?', [ 'NumCampaigns' => $numCampaigns ]);
1321
        } elseif (count($descendants) > 0) {
1322
            $archiveWarningMsg = _t('SilverStripe\\CMS\\Controllers\\CMSMain.ArchiveWarningWithChildren', 'Warning: This page and all of its child pages will be unpublished before being sent to the archive.\n\nAre you sure you want to proceed?');
1323
        } elseif ($inChangeSets->count() > 0) {
1324
            $archiveWarningMsg = _t('SilverStripe\\CMS\\Controllers\\CMSMain.ArchiveWarningWithCampaigns', 'Warning: This page will be unpublished and automatically removed from their associated {NumCampaigns} before being sent to the archive.\n\nAre you sure you want to proceed?', [ 'NumCampaigns' => $numCampaigns ]);
1325
        } else {
1326
            $archiveWarningMsg = _t('SilverStripe\\CMS\\Controllers\\CMSMain.ArchiveWarning', 'Warning: This page will be unpublished before being sent to the archive.\n\nAre you sure you want to proceed?');
1327
        }
1328
1329
        return $archiveWarningMsg;
1330
    }
1331
1332
    /**
1333
     * This method exclusively handles deferred ajax requests to render the
1334
     * pages tree deferred handler (no pjax-fragment)
1335
     *
1336
     * @return string HTML
1337
     */
1338
    public function treeview()
1339
    {
1340
        return $this->renderWith($this->getTemplatesWithSuffix('_TreeView'));
1341
    }
1342
1343
    /**
1344
     * Note: This method exclusively handles top level view of list view
1345
     *
1346
     * @param HTTPRequest $request
1347
     * @return string HTML
1348
     */
1349
    public function listview($request)
1350
    {
1351
        // Ensure selected page ID is highlighted
1352
        $pageID = $request->param('ParentID') ?: 0;
1353
        $this->setCurrentPageID($pageID);
1354
        return $this->getResponseNegotiator()->respond($request);
1355
    }
1356
1357
    /**
1358
     * @param string $default
1359
     * @return string
1360
     */
1361
    public function ViewState($default = 'treeview')
1362
    {
1363
        $mode = $this->getRequest()->requestVar('view')
1364
            ?: $this->getRequest()->param('Action');
1365
        switch ($mode) {
1366
            case 'listview':
1367
            case 'treeview':
1368
                return $mode;
1369
            default:
1370
                return $default;
1371
        }
1372
    }
1373
1374
    /**
1375
     * Callback to request the list of page types allowed under a given page instance.
1376
     * Provides a slower but more precise response over SiteTreeHints
1377
     *
1378
     * @param HTTPRequest $request
1379
     * @return HTTPResponse
1380
     */
1381
    public function childfilter($request)
1382
    {
1383
        // Check valid parent specified
1384
        $parentID = $request->requestVar('ParentID');
1385
        $parent = SiteTree::get()->byID($parentID);
1386
        if (!$parent || !$parent->exists()) {
1387
            return $this->httpError(404);
1388
        }
1389
1390
        // Build hints specific to this class
1391
        // Identify disallows and set globals
1392
        $classes = SiteTree::page_type_classes();
1393
        $disallowedChildren = array();
1394
        foreach ($classes as $class) {
1395
            $obj = singleton($class);
1396
            if ($obj instanceof HiddenClass) {
1397
                continue;
1398
            }
1399
1400
            if (!$obj->canCreate(null, array('Parent' => $parent))) {
1401
                $disallowedChildren[] = $class;
1402
            }
1403
        }
1404
1405
        $this->extend('updateChildFilter', $disallowedChildren, $parentID);
1406
        return $this
1407
            ->getResponse()
1408
            ->addHeader('Content-Type', 'application/json; charset=utf-8')
1409
            ->setBody(Convert::raw2json($disallowedChildren));
1410
    }
1411
1412
    /**
1413
     * Safely reconstruct a selected filter from a given set of query parameters
1414
     *
1415
     * @param array $params Query parameters to use
1416
     * @return CMSSiteTreeFilter The filter class, or null if none present
1417
     * @throws InvalidArgumentException if invalid filter class is passed.
1418
     */
1419
    protected function getQueryFilter($params)
1420
    {
1421
        if (empty($params['FilterClass'])) {
1422
            return null;
1423
        }
1424
        $filterClass = $params['FilterClass'];
1425
        if (!is_subclass_of($filterClass, CMSSiteTreeFilter::class)) {
0 ignored issues
show
Bug introduced by
Due to PHP Bug #53727, is_subclass_of might return inconsistent results on some PHP versions if \SilverStripe\CMS\Contro...MSSiteTreeFilter::class can be an interface. If so, you could instead use ReflectionClass::implementsInterface.
Loading history...
1426
            throw new InvalidArgumentException("Invalid filter class passed: {$filterClass}");
1427
        }
1428
        return $filterClass::create($params);
1429
    }
1430
1431
    /**
1432
     * Returns the pages meet a certain criteria as {@see CMSSiteTreeFilter} or the subpages of a parent page
1433
     * defaulting to no filter and show all pages in first level.
1434
     * Doubles as search results, if any search parameters are set through {@link SearchForm()}.
1435
     *
1436
     * @param array $params Search filter criteria
1437
     * @param int $parentID Optional parent node to filter on (can't be combined with other search criteria)
1438
     * @return SS_List
1439
     * @throws InvalidArgumentException if invalid filter class is passed.
1440
     */
1441
    public function getList($params = array(), $parentID = 0)
1442
    {
1443
        if ($filter = $this->getQueryFilter($params)) {
1444
            return $filter->getFilteredPages();
1445
        } else {
1446
            $list = DataList::create($this->config()->get('tree_class'));
1447
            $parentID = is_numeric($parentID) ? $parentID : 0;
1448
            return $list->filter("ParentID", $parentID);
1449
        }
1450
    }
1451
1452
    /**
1453
     * @return Form
1454
     */
1455
    public function ListViewForm()
1456
    {
1457
        $params = $this->getRequest()->requestVar('q');
1458
        $parentID = $this->getRequest()->param('ParentID');
1459
        $list = $this->getList($params, $parentID);
1460
        $gridFieldConfig = GridFieldConfig::create()->addComponents(
1461
            new GridFieldSortableHeader(),
1462
            new GridFieldDataColumns(),
1463
            new GridFieldPaginator($this->config()->get('page_length'))
1464
        );
1465
        if ($parentID) {
1466
            $linkSpec = $this->LinkListViewChildren('%d');
1467
            $gridFieldConfig->addComponent(
1468
                GridFieldLevelup::create($parentID)
1469
                    ->setLinkSpec($linkSpec)
1470
                    ->setAttributes(array('data-pjax' => 'ListViewForm,Breadcrumbs'))
1471
            );
1472
        }
1473
        $gridField = new GridField('Page', 'Pages', $list, $gridFieldConfig);
1474
        $gridField->setAttribute('cms-loading-ignore-url-params', true);
0 ignored issues
show
Documentation introduced by
true is of type boolean, but the function expects a string.

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1475
        /** @var GridFieldDataColumns $columns */
1476
        $columns = $gridField->getConfig()->getComponentByType('SilverStripe\\Forms\\GridField\\GridFieldDataColumns');
1477
1478
        // Don't allow navigating into children nodes on filtered lists
1479
        $fields = array(
1480
            'getTreeTitle' => _t('SilverStripe\\CMS\\Model\\SiteTree.PAGETITLE', 'Page Title'),
1481
            'singular_name' => _t('SilverStripe\\CMS\\Model\\SiteTree.PAGETYPE', 'Page Type'),
1482
            'LastEdited' => _t('SilverStripe\\CMS\\Model\\SiteTree.LASTUPDATED', 'Last Updated'),
1483
        );
1484
        /** @var GridFieldSortableHeader $sortableHeader */
1485
        $sortableHeader = $gridField->getConfig()->getComponentByType('SilverStripe\\Forms\\GridField\\GridFieldSortableHeader');
1486
        $sortableHeader->setFieldSorting(array('getTreeTitle' => 'Title'));
1487
        $gridField->getState()->ParentID = $parentID;
1488
1489
        if (!$params) {
1490
            $fields = array_merge(array('listChildrenLink' => ''), $fields);
1491
        }
1492
1493
        $columns->setDisplayFields($fields);
1494
        $columns->setFieldCasting(array(
1495
            'Created' => 'DBDatetime->Ago',
1496
            'LastEdited' => 'DBDatetime->FormatFromSettings',
1497
            'getTreeTitle' => 'HTMLFragment'
1498
        ));
1499
1500
        $controller = $this;
1501
        $columns->setFieldFormatting(array(
1502
            'listChildrenLink' => function ($value, &$item) use ($controller) {
1503
                /** @var SiteTree $item */
1504
                $num = $item ? $item->numChildren() : null;
0 ignored issues
show
Documentation Bug introduced by
The method numChildren does not exist on object<SilverStripe\CMS\Model\SiteTree>? Since you implemented __call, maybe consider adding a @method annotation.

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

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

class ParentClass {
    private $data = array();

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

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

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
1505
                if ($num) {
1506
                    return sprintf(
1507
                        '<a class="btn btn-secondary btn--no-text btn--icon-large font-icon-right-dir cms-panel-link list-children-link" data-pjax-target="ListViewForm,Breadcrumbs" href="%s"><span class="sr-only">%s child pages</span></a>',
1508
                        $this->LinkListViewChildren((int)$item->ID),
1509
                        $num
1510
                    );
1511
                }
1512
            },
1513
            'getTreeTitle' => function ($value, &$item) use ($controller) {
1514
                $title = sprintf(
1515
                    '<a class="action-detail" href="%s">%s</a>',
1516
                    Controller::join_links(
1517
                        CMSPageEditController::singleton()->Link('show'),
1518
                        (int)$item->ID
1519
                    ),
1520
                    $item->TreeTitle // returns HTML, does its own escaping
1521
                );
1522
                $breadcrumbs = $item->Breadcrumbs(20, true, false, true, '/');
1523
                // Remove item's tile
1524
                $breadcrumbs = preg_replace('/[^\/]+$/', '', trim($breadcrumbs));
1525
                // Trim spaces around delimiters
1526
                $breadcrumbs = preg_replace('/\s?\/\s?/', '/', trim($breadcrumbs));
1527
                return $title . sprintf('<p class="small cms-list__item-breadcrumbs">%s</p>', $breadcrumbs);
1528
            }
1529
        ));
1530
1531
        $negotiator = $this->getResponseNegotiator();
1532
        $listview = Form::create(
1533
            $this,
1534
            'ListViewForm',
1535
            new FieldList($gridField),
1536
            new FieldList()
1537
        )->setHTMLID('Form_ListViewForm');
1538
        $listview->setAttribute('data-pjax-fragment', 'ListViewForm');
1539 View Code Duplication
        $listview->setValidationResponseCallback(function (ValidationResult $errors) use ($negotiator, $listview) {
0 ignored issues
show
Unused Code introduced by
The parameter $errors is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
1540
            $request = $this->getRequest();
1541
            if ($request->isAjax() && $negotiator) {
1542
                $result = $listview->forTemplate();
1543
                return $negotiator->respond($request, array(
1544
                    'CurrentForm' => function () use ($result) {
1545
                        return $result;
1546
                    }
1547
                ));
1548
            }
1549
        });
1550
1551
        $this->extend('updateListView', $listview);
1552
1553
        $listview->disableSecurityToken();
1554
        return $listview;
1555
    }
1556
1557
    public function currentPageID()
1558
    {
1559
        $id = parent::currentPageID();
1560
1561
        $this->extend('updateCurrentPageID', $id);
1562
1563
        return $id;
1564
    }
1565
1566
    //------------------------------------------------------------------------------------------//
1567
    // Data saving handlers
1568
1569
    /**
1570
     * Save and Publish page handler
1571
     *
1572
     * @param array $data
1573
     * @param Form $form
1574
     * @return HTTPResponse
1575
     * @throws HTTPResponse_Exception
1576
     */
1577
    public function save($data, $form)
1578
    {
1579
        $className = $this->config()->get('tree_class');
1580
1581
        // Existing or new record?
1582
        $id = $data['ID'];
1583
        if (substr($id, 0, 3) != 'new') {
1584
            /** @var SiteTree $record */
1585
            $record = DataObject::get_by_id($className, $id);
1586
            // Check edit permissions
1587
            if ($record && !$record->canEdit()) {
1588
                return Security::permissionFailure($this);
1589
            }
1590
            if (!$record || !$record->ID) {
1591
                throw new HTTPResponse_Exception("Bad record ID #$id", 404);
1592
            }
1593
        } else {
1594
            if (!$className::singleton()->canCreate()) {
1595
                return Security::permissionFailure($this);
1596
            }
1597
            $record = $this->getNewItem($id, false);
1598
        }
1599
1600
        // Check publishing permissions
1601
        $doPublish = !empty($data['publish']);
1602
        if ($record && $doPublish && !$record->canPublish()) {
1603
            return Security::permissionFailure($this);
1604
        }
1605
1606
        // TODO Coupling to SiteTree
1607
        $record->HasBrokenLink = 0;
1608
        $record->HasBrokenFile = 0;
1609
1610
        if (!$record->ObsoleteClassName) {
1611
            $record->writeWithoutVersion();
1612
        }
1613
1614
        // Update the class instance if necessary
1615
        if (isset($data['ClassName']) && $data['ClassName'] != $record->ClassName) {
1616
            // Replace $record with a new instance of the new class
1617
            $newClassName = $data['ClassName'];
1618
            $record = $record->newClassInstance($newClassName);
1619
        }
1620
1621
        // save form data into record
1622
        $form->saveInto($record);
1623
        $record->write();
1624
1625
        // If the 'Save & Publish' button was clicked, also publish the page
1626
        if ($doPublish) {
1627
            $record->publishRecursive();
1628
            $message = _t(
1629
                'SilverStripe\\CMS\\Controllers\\CMSMain.PUBLISHED',
1630
                "Published '{title}' successfully.",
1631
                ['title' => $record->Title]
1632
            );
1633
        } else {
1634
            $message = _t(
1635
                'SilverStripe\\CMS\\Controllers\\CMSMain.SAVED',
1636
                "Saved '{title}' successfully.",
1637
                ['title' => $record->Title]
1638
            );
1639
        }
1640
1641
        $this->getResponse()->addHeader('X-Status', rawurlencode($message));
1642
        return $this->getResponseNegotiator()->respond($this->getRequest());
1643
    }
1644
1645
    /**
1646
     * @uses LeftAndMainExtension->augmentNewSiteTreeItem()
1647
     *
1648
     * @param int|string $id
1649
     * @param bool $setID
1650
     * @return mixed|DataObject
1651
     * @throws HTTPResponse_Exception
1652
     */
1653
    public function getNewItem($id, $setID = true)
1654
    {
1655
        $parentClass = $this->config()->get('tree_class');
1656
        list(, $className, $parentID) = array_pad(explode('-', $id), 3, null);
1657
1658
        if (!is_a($className, $parentClass, true)) {
1659
            $response = Security::permissionFailure($this);
1660
            if (!$response) {
1661
                $response = $this->getResponse();
1662
            }
1663
            throw new HTTPResponse_Exception($response);
1664
        }
1665
1666
        /** @var SiteTree $newItem */
1667
        $newItem = Injector::inst()->create($className);
1668
        $newItem->Title = _t(
1669
            'SilverStripe\\CMS\\Controllers\\CMSMain.NEWPAGE',
1670
            "New {pagetype}",
1671
            'followed by a page type title',
1672
            array('pagetype' => singleton($className)->i18n_singular_name())
1673
        );
1674
        $newItem->ClassName = $className;
1675
        $newItem->ParentID = $parentID;
0 ignored issues
show
Documentation introduced by
The property ParentID does not exist on object<SilverStripe\CMS\Model\SiteTree>. Since you implemented __set, maybe consider adding a @property annotation.

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

<?php

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

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

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

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

}

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

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

See also the PhpDoc documentation for @property.

Loading history...
1676
1677
        // DataObject::fieldExists only checks the current class, not the hierarchy
1678
        // This allows the CMS to set the correct sort value
1679
        if ($newItem->castingHelper('Sort')) {
1680
            $maxSort = DB::prepared_query(
1681
                'SELECT MAX("Sort") FROM "SiteTree" WHERE "ParentID" = ?',
1682
                array($parentID)
1683
            )->value();
1684
            $newItem->Sort = (int)$maxSort + 1;
1685
        }
1686
1687
        if ($setID && $id) {
1688
            $newItem->ID = $id;
1689
        }
1690
1691
        # Some modules like subsites add extra fields that need to be set when the new item is created
1692
        $this->extend('augmentNewSiteTreeItem', $newItem);
1693
1694
        return $newItem;
1695
    }
1696
1697
    /**
1698
     * Actually perform the publication step
1699
     *
1700
     * @param Versioned|DataObject $record
1701
     * @return mixed
1702
     */
1703
    public function performPublish($record)
1704
    {
1705
        if ($record && !$record->canPublish()) {
0 ignored issues
show
Bug introduced by
The method canPublish does only exist in SilverStripe\Versioned\Versioned, but not in SilverStripe\ORM\DataObject.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
1706
            return Security::permissionFailure($this);
1707
        }
1708
1709
        $record->publishRecursive();
0 ignored issues
show
Bug introduced by
The method publishRecursive does only exist in SilverStripe\Versioned\Versioned, but not in SilverStripe\ORM\DataObject.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
1710
    }
1711
1712
    /**
1713
     * Reverts a page by publishing it to live.
1714
     * Use {@link restorepage()} if you want to restore a page
1715
     * which was deleted from draft without publishing.
1716
     *
1717
     * @uses SiteTree->doRevertToLive()
1718
     *
1719
     * @param array $data
1720
     * @param Form $form
1721
     * @return HTTPResponse
1722
     * @throws HTTPResponse_Exception
1723
     */
1724
    public function revert($data, $form)
0 ignored issues
show
Unused Code introduced by
The parameter $form is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
1725
    {
1726
        if (!isset($data['ID'])) {
1727
            throw new HTTPResponse_Exception("Please pass an ID in the form content", 400);
1728
        }
1729
1730
        $id = (int) $data['ID'];
1731
        $restoredPage = Versioned::get_latest_version(SiteTree::class, $id);
1732
        if (!$restoredPage) {
1733
            throw new HTTPResponse_Exception("SiteTree #$id not found", 400);
1734
        }
1735
1736
        /** @var SiteTree $record */
1737
        $record = Versioned::get_one_by_stage(SiteTree::class, Versioned::LIVE, array(
1738
            '"SiteTree_Live"."ID"' => $id
1739
        ));
1740
1741
        // a user can restore a page without publication rights, as it just adds a new draft state
1742
        // (this action should just be available when page has been "deleted from draft")
1743
        if ($record && !$record->canEdit()) {
1744
            return Security::permissionFailure($this);
1745
        }
1746
        if (!$record || !$record->ID) {
1747
            throw new HTTPResponse_Exception("Bad record ID #$id", 404);
1748
        }
1749
1750
        $record->doRevertToLive();
0 ignored issues
show
Documentation Bug introduced by
The method doRevertToLive does not exist on object<SilverStripe\CMS\Model\SiteTree>? Since you implemented __call, maybe consider adding a @method annotation.

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

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

class ParentClass {
    private $data = array();

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

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

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
1751
1752
        $this->getResponse()->addHeader(
1753
            'X-Status',
1754
            rawurlencode(_t(
1755
                'SilverStripe\\CMS\\Controllers\\CMSMain.RESTORED',
1756
                "Restored '{title}' successfully",
1757
                'Param {title} is a title',
1758
                array('title' => $record->Title)
1759
            ))
1760
        );
1761
1762
        return $this->getResponseNegotiator()->respond($this->getRequest());
1763
    }
1764
1765
    /**
1766
     * Delete the current page from draft stage.
1767
     *
1768
     * @see deletefromlive()
1769
     *
1770
     * @param array $data
1771
     * @param Form $form
1772
     * @return HTTPResponse
1773
     * @throws HTTPResponse_Exception
1774
     */
1775 View Code Duplication
    public function delete($data, $form)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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

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

Loading history...
1776
    {
1777
        $id = $data['ID'];
1778
        $record = SiteTree::get()->byID($id);
1779
        if ($record && !$record->canDelete()) {
1780
            return Security::permissionFailure();
1781
        }
1782
        if (!$record || !$record->ID) {
1783
            throw new HTTPResponse_Exception("Bad record ID #$id", 404);
1784
        }
1785
1786
        // Delete record
1787
        $record->delete();
1788
1789
        $this->getResponse()->addHeader(
1790
            'X-Status',
1791
            rawurlencode(_t(
1792
                __CLASS__ . '.REMOVEDPAGEFROMDRAFT',
1793
                "Removed '{title}' from the draft site",
1794
                ['title' => $record->Title]
1795
            ))
1796
        );
1797
1798
        // Even if the record has been deleted from stage and live, it can be viewed in "archive mode"
1799
        return $this->getResponseNegotiator()->respond($this->getRequest());
1800
    }
1801
1802
    /**
1803
     * Delete this page from both live and stage
1804
     *
1805
     * @param array $data
1806
     * @param Form $form
1807
     * @return HTTPResponse
1808
     * @throws HTTPResponse_Exception
1809
     */
1810 View Code Duplication
    public function archive($data, $form)
0 ignored issues
show
Unused Code introduced by
The parameter $form is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Duplication introduced by
This method seems to be duplicated in your project.

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

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

Loading history...
1811
    {
1812
        $id = $data['ID'];
1813
        /** @var SiteTree $record */
1814
        $record = SiteTree::get()->byID($id);
1815
        if (!$record || !$record->exists()) {
1816
            throw new HTTPResponse_Exception("Bad record ID #$id", 404);
1817
        }
1818
        if (!$record->canArchive()) {
0 ignored issues
show
Documentation Bug introduced by
The method canArchive does not exist on object<SilverStripe\CMS\Model\SiteTree>? Since you implemented __call, maybe consider adding a @method annotation.

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

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

class ParentClass {
    private $data = array();

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

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

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

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

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

class ParentClass {
    private $data = array();

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

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

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
1824
1825
        $this->getResponse()->addHeader(
1826
            'X-Status',
1827
            rawurlencode(_t(
1828
                __CLASS__ . '.ARCHIVEDPAGE',
1829
                "Archived page '{title}'",
1830
                ['title' => $record->Title]
1831
            ))
1832
        );
1833
1834
        // Even if the record has been deleted from stage and live, it can be viewed in "archive mode"
1835
        return $this->getResponseNegotiator()->respond($this->getRequest());
1836
    }
1837
1838
    public function publish($data, $form)
1839
    {
1840
        $data['publish'] = '1';
1841
1842
        return $this->save($data, $form);
1843
    }
1844
1845 View Code Duplication
    public function unpublish($data, $form)
0 ignored issues
show
Unused Code introduced by
The parameter $form is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Duplication introduced by
This method seems to be duplicated in your project.

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

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

Loading history...
1846
    {
1847
        $className = $this->config()->get('tree_class');
1848
        /** @var SiteTree $record */
1849
        $record = DataObject::get_by_id($className, $data['ID']);
1850
1851
        if ($record && !$record->canUnpublish()) {
0 ignored issues
show
Documentation Bug introduced by
The method canUnpublish does not exist on object<SilverStripe\CMS\Model\SiteTree>? Since you implemented __call, maybe consider adding a @method annotation.

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

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

class ParentClass {
    private $data = array();

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

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

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
1852
            return Security::permissionFailure($this);
1853
        }
1854
        if (!$record || !$record->ID) {
1855
            throw new HTTPResponse_Exception("Bad record ID #" . (int)$data['ID'], 404);
1856
        }
1857
1858
        $record->doUnpublish();
0 ignored issues
show
Documentation Bug introduced by
The method doUnpublish does not exist on object<SilverStripe\CMS\Model\SiteTree>? Since you implemented __call, maybe consider adding a @method annotation.

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

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

class ParentClass {
    private $data = array();

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

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

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
1859
1860
        $this->getResponse()->addHeader(
1861
            'X-Status',
1862
            rawurlencode(_t('SilverStripe\\CMS\\Controllers\\CMSMain.REMOVEDPAGE', "Removed '{title}' from the published site", array('title' => $record->Title)))
1863
        );
1864
1865
        return $this->getResponseNegotiator()->respond($this->getRequest());
1866
    }
1867
1868
    /**
1869
     * @return HTTPResponse
1870
     */
1871
    public function rollback()
1872
    {
1873
        return $this->doRollback(array(
1874
            'ID' => $this->currentPageID(),
1875
            'Version' => $this->getRequest()->param('VersionID')
1876
        ), null);
0 ignored issues
show
Documentation introduced by
null is of type null, but the function expects a object<SilverStripe\Forms\Form>.

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1877
    }
1878
1879
    /**
1880
     * Rolls a site back to a given version ID
1881
     *
1882
     * @param array $data
1883
     * @param Form $form
1884
     * @return HTTPResponse
1885
     */
1886
    public function doRollback($data, $form)
0 ignored issues
show
Unused Code introduced by
The parameter $form is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
1887
    {
1888
        $this->extend('onBeforeRollback', $data['ID']);
1889
1890
        $id = (isset($data['ID'])) ? (int) $data['ID'] : null;
1891
        $version = (isset($data['Version'])) ? (int) $data['Version'] : null;
1892
1893
        /** @var DataObject|Versioned $record */
1894
        $record = DataObject::get_by_id($this->config()->get('tree_class'), $id);
1895
        if ($record && !$record->canEdit()) {
0 ignored issues
show
Bug introduced by
The method canEdit does only exist in SilverStripe\ORM\DataObject, but not in SilverStripe\Versioned\Versioned.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
1896
            return Security::permissionFailure($this);
1897
        }
1898
1899
        if ($version) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $version of type integer|null is loosely compared to true; this is ambiguous if the integer can be zero. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
1900
            $record->doRollbackTo($version);
0 ignored issues
show
Bug introduced by
The method doRollbackTo does only exist in SilverStripe\Versioned\Versioned, but not in SilverStripe\ORM\DataObject.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
1901
            $message = _t(
1902
                __CLASS__ . '.ROLLEDBACKVERSIONv2',
1903
                "Rolled back to version #{version}.",
1904
                array('version' => $data['Version'])
1905
            );
1906
        } else {
1907
            $record->doRevertToLive();
0 ignored issues
show
Bug introduced by
The method doRevertToLive does only exist in SilverStripe\Versioned\Versioned, but not in SilverStripe\ORM\DataObject.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
1908
            $message = _t(
1909
                __CLASS__ . '.ROLLEDBACKPUBv2',
1910
                "Rolled back to published version."
1911
            );
1912
        }
1913
1914
        $this->getResponse()->addHeader('X-Status', rawurlencode($message));
1915
1916
        // Can be used in different contexts: In normal page edit view, in which case the redirect won't have any effect.
1917
        // Or in history view, in which case a revert causes the CMS to re-load the edit view.
1918
        // The X-Pjax header forces a "full" content refresh on redirect.
1919
        $url = Controller::join_links(CMSPageEditController::singleton()->Link('show'), $record->ID);
1920
        $this->getResponse()->addHeader('X-ControllerURL', $url);
1921
        $this->getRequest()->addHeader('X-Pjax', 'Content');
1922
        $this->getResponse()->addHeader('X-Pjax', 'Content');
1923
1924
        return $this->getResponseNegotiator()->respond($this->getRequest());
1925
    }
1926
1927
    /**
1928
     * Batch Actions Handler
1929
     */
1930
    public function batchactions()
1931
    {
1932
        return new CMSBatchActionHandler($this, 'batchactions');
1933
    }
1934
1935
    public function BatchActionParameters()
1936
    {
1937
        $batchActions = CMSBatchActionHandler::config()->batch_actions;
1938
1939
        $forms = array();
1940
        foreach ($batchActions as $urlSegment => $batchAction) {
1941
            $SNG_action = singleton($batchAction);
1942
            if ($SNG_action->canView() && $fieldset = $SNG_action->getParameterFields()) {
1943
                $formHtml = '';
1944
                /** @var FormField $field */
1945
                foreach ($fieldset as $field) {
1946
                    $formHtml .= $field->Field();
1947
                }
1948
                $forms[$urlSegment] = $formHtml;
1949
            }
1950
        }
1951
        $pageHtml = '';
1952
        foreach ($forms as $urlSegment => $html) {
1953
            $pageHtml .= "<div class=\"params\" id=\"BatchActionParameters_$urlSegment\">$html</div>\n\n";
1954
        }
1955
        return new LiteralField("BatchActionParameters", '<div id="BatchActionParameters" style="display:none">'.$pageHtml.'</div>');
1956
    }
1957
    /**
1958
     * Returns a list of batch actions
1959
     */
1960
    public function BatchActionList()
1961
    {
1962
        return $this->batchactions()->batchActionList();
1963
    }
1964
1965
    public function publishall($request)
1966
    {
1967
        if (!Permission::check('ADMIN')) {
1968
            return Security::permissionFailure($this);
1969
        }
1970
1971
        Environment::increaseTimeLimitTo();
1972
        Environment::increaseMemoryLimitTo();
1973
1974
        $response = "";
1975
1976
        if (isset($this->requestParams['confirm'])) {
1977
            // Protect against CSRF on destructive action
1978
            if (!SecurityToken::inst()->checkRequest($request)) {
1979
                return $this->httpError(400);
1980
            }
1981
1982
            $start = 0;
1983
            $pages = SiteTree::get()->limit("$start,30");
1984
            $count = 0;
1985
            while ($pages) {
1986
                /** @var SiteTree $page */
1987
                foreach ($pages as $page) {
1988
                    if ($page && !$page->canPublish()) {
1989
                        return Security::permissionFailure($this);
1990
                    }
1991
1992
                    $page->publishRecursive();
1993
                    $page->destroy();
1994
                    unset($page);
1995
                    $count++;
1996
                    $response .= "<li>$count</li>";
1997
                }
1998
                if ($pages->count() > 29) {
1999
                    $start += 30;
2000
                    $pages = SiteTree::get()->limit("$start,30");
2001
                } else {
2002
                    break;
2003
                }
2004
            }
2005
            $response .= _t('SilverStripe\\CMS\\Controllers\\CMSMain.PUBPAGES', "Done: Published {count} pages", array('count' => $count));
2006
        } else {
2007
            $token = SecurityToken::inst();
2008
            $fields = new FieldList();
2009
            $token->updateFieldSet($fields);
2010
            $tokenField = $fields->first();
2011
            $tokenHtml = ($tokenField) ? $tokenField->FieldHolder() : '';
2012
            $publishAllDescription = _t(
2013
                'SilverStripe\\CMS\\Controllers\\CMSMain.PUBALLFUN2',
2014
                'Pressing this button will do the equivalent of going to every page and pressing "publish".  '
2015
                . 'It\'s intended to be used after there have been massive edits of the content, such as when '
2016
                . 'the site was first built.'
2017
            );
2018
            $response .= '<h1>' . _t('SilverStripe\\CMS\\Controllers\\CMSMain.PUBALLFUN', '"Publish All" functionality') . '</h1>
2019
				<p>' . $publishAllDescription . '</p>
2020
				<form method="post" action="publishall">
2021
					<input type="submit" name="confirm" value="'
2022
                    . _t('SilverStripe\\CMS\\Controllers\\CMSMain.PUBALLCONFIRM', "Please publish every page in the site, copying content stage to live", 'Confirmation button') .'" />'
2023
                    . $tokenHtml .
2024
                '</form>';
2025
        }
2026
2027
        return $response;
2028
    }
2029
2030
    /**
2031
     * Restore a completely deleted page from the SiteTree_versions table.
2032
     *
2033
     * @param array $data
2034
     * @param Form $form
2035
     * @return HTTPResponse
2036
     */
2037
    public function restore($data, $form)
0 ignored issues
show
Unused Code introduced by
The parameter $form is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
2038
    {
2039
        if (!isset($data['ID']) || !is_numeric($data['ID'])) {
2040
            return new HTTPResponse("Please pass an ID in the form content", 400);
2041
        }
2042
2043
        $id = (int)$data['ID'];
2044
        /** @var SiteTree $restoredPage */
2045
        $restoredPage = Versioned::get_latest_version(SiteTree::class, $id);
2046
        if (!$restoredPage) {
2047
            return new HTTPResponse("SiteTree #$id not found", 400);
2048
        }
2049
2050
        $restoredPage = $restoredPage->doRestoreToStage();
2051
2052
        $this->getResponse()->addHeader(
2053
            'X-Status',
2054
            rawurlencode(_t(
2055
                'SilverStripe\\CMS\\Controllers\\CMSMain.RESTORED',
2056
                "Restored '{title}' successfully",
2057
                array('title' => $restoredPage->Title)
2058
            ))
2059
        );
2060
2061
        return $this->getResponseNegotiator()->respond($this->getRequest());
2062
    }
2063
2064
    public function duplicate($request)
2065
    {
2066
        // Protect against CSRF on destructive action
2067
        if (!SecurityToken::inst()->checkRequest($request)) {
2068
            return $this->httpError(400);
2069
        }
2070
2071
        if (($id = $this->urlParams['ID']) && is_numeric($id)) {
2072
            /** @var SiteTree $page */
2073
            $page = SiteTree::get()->byID($id);
2074 View Code Duplication
            if ($page && (!$page->canEdit() || !$page->canCreate(null, array('Parent' => $page->Parent())))) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
2075
                return Security::permissionFailure($this);
2076
            }
2077
            if (!$page || !$page->ID) {
2078
                throw new HTTPResponse_Exception("Bad record ID #$id", 404);
2079
            }
2080
2081
            $newPage = $page->duplicate();
2082
2083
            // ParentID can be hard-set in the URL.  This is useful for pages with multiple parents
2084
            if (isset($_GET['parentID']) && is_numeric($_GET['parentID'])) {
2085
                $newPage->ParentID = $_GET['parentID'];
0 ignored issues
show
Documentation introduced by
The property ParentID does not exist on object<SilverStripe\CMS\Model\SiteTree>. Since you implemented __set, maybe consider adding a @property annotation.

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

<?php

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

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

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

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

}

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

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

See also the PhpDoc documentation for @property.

Loading history...
2086
                $newPage->write();
2087
            }
2088
2089
            $this->getResponse()->addHeader(
2090
                'X-Status',
2091
                rawurlencode(_t(
2092
                    'SilverStripe\\CMS\\Controllers\\CMSMain.DUPLICATED',
2093
                    "Duplicated '{title}' successfully",
2094
                    array('title' => $newPage->Title)
2095
                ))
2096
            );
2097
            $url = Controller::join_links(CMSPageEditController::singleton()->Link('show'), $newPage->ID);
2098
            $this->getResponse()->addHeader('X-ControllerURL', $url);
2099
            $this->getRequest()->addHeader('X-Pjax', 'Content');
2100
            $this->getResponse()->addHeader('X-Pjax', 'Content');
2101
2102
            return $this->getResponseNegotiator()->respond($this->getRequest());
2103
        } else {
2104
            return new HTTPResponse("CMSMain::duplicate() Bad ID: '$id'", 400);
2105
        }
2106
    }
2107
2108
    public function duplicatewithchildren($request)
2109
    {
2110
        // Protect against CSRF on destructive action
2111
        if (!SecurityToken::inst()->checkRequest($request)) {
2112
            return $this->httpError(400);
2113
        }
2114
        Environment::increaseTimeLimitTo();
2115
        if (($id = $this->urlParams['ID']) && is_numeric($id)) {
2116
            /** @var SiteTree $page */
2117
            $page = SiteTree::get()->byID($id);
2118 View Code Duplication
            if ($page && (!$page->canEdit() || !$page->canCreate(null, array('Parent' => $page->Parent())))) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
2119
                return Security::permissionFailure($this);
2120
            }
2121
            if (!$page || !$page->ID) {
2122
                throw new HTTPResponse_Exception("Bad record ID #$id", 404);
2123
            }
2124
2125
            $newPage = $page->duplicateWithChildren();
2126
2127
            $this->getResponse()->addHeader(
2128
                'X-Status',
2129
                rawurlencode(_t(
2130
                    'SilverStripe\\CMS\\Controllers\\CMSMain.DUPLICATEDWITHCHILDREN',
2131
                    "Duplicated '{title}' and children successfully",
2132
                    array('title' => $newPage->Title)
2133
                ))
2134
            );
2135
            $url = Controller::join_links(CMSPageEditController::singleton()->Link('show'), $newPage->ID);
2136
            $this->getResponse()->addHeader('X-ControllerURL', $url);
2137
            $this->getRequest()->addHeader('X-Pjax', 'Content');
2138
            $this->getResponse()->addHeader('X-Pjax', 'Content');
2139
2140
            return $this->getResponseNegotiator()->respond($this->getRequest());
2141
        } else {
2142
            return new HTTPResponse("CMSMain::duplicatewithchildren() Bad ID: '$id'", 400);
2143
        }
2144
    }
2145
2146
    public function providePermissions()
2147
    {
2148
        $title = CMSPagesController::menu_title();
2149
        return array(
2150
            "CMS_ACCESS_CMSMain" => array(
2151
                'name' => _t('SilverStripe\\CMS\\Controllers\\CMSMain.ACCESS', "Access to '{title}' section", array('title' => $title)),
2152
                'category' => _t('SilverStripe\\Security\\Permission.CMS_ACCESS_CATEGORY', 'CMS Access'),
2153
                'help' => _t(
2154
                    'SilverStripe\\CMS\\Controllers\\CMSMain.ACCESS_HELP',
2155
                    'Allow viewing of the section containing page tree and content. View and edit permissions can be handled through page specific dropdowns, as well as the separate "Content permissions".'
2156
                ),
2157
                'sort' => -99 // below "CMS_ACCESS_LeftAndMain", but above everything else
2158
            )
2159
        );
2160
    }
2161
2162
    /**
2163
     * Get title for root CMS node
2164
     *
2165
     * @return string
2166
     */
2167
    protected function getCMSTreeTitle()
2168
    {
2169
        $rootTitle = SiteConfig::current_site_config()->Title;
2170
        $this->extend('updateCMSTreeTitle', $rootTitle);
2171
        return $rootTitle;
2172
    }
2173
}
2174