Completed
Pull Request — master (#1783)
by Damian
02:29
created

CMSMain::getsubtree()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 17
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 17
rs 9.4285
c 0
b 0
f 0
cc 1
eloc 11
nc 1
nop 1
1
<?php
2
3
namespace SilverStripe\CMS\Controllers;
4
5
use SilverStripe\Admin\AdminRootController;
6
use SilverStripe\Admin\CMSBatchActionHandler;
7
use SilverStripe\Admin\LeftAndMainFormRequestHandler;
8
use SilverStripe\CMS\Model\VirtualPage;
9
use SilverStripe\Forms\Tab;
10
use SilverStripe\ORM\CMSPreviewable;
11
use SilverStripe\Admin\LeftAndMain;
12
use SilverStripe\CMS\BatchActions\CMSBatchAction_Archive;
13
use SilverStripe\CMS\BatchActions\CMSBatchAction_Publish;
14
use SilverStripe\CMS\BatchActions\CMSBatchAction_Restore;
15
use SilverStripe\CMS\BatchActions\CMSBatchAction_Unpublish;
16
use SilverStripe\CMS\Model\CurrentPageIdentifier;
17
use SilverStripe\CMS\Model\RedirectorPage;
18
use SilverStripe\CMS\Model\SiteTree;
19
use SilverStripe\Control\Controller;
20
use SilverStripe\Control\Director;
21
use SilverStripe\Control\Session;
22
use SilverStripe\Control\HTTPRequest;
23
use SilverStripe\Control\HTTPResponse;
24
use SilverStripe\Control\HTTPResponse_Exception;
25
use SilverStripe\Core\Convert;
26
use SilverStripe\Core\Injector\Injector;
27
use Psr\SimpleCache\CacheInterface;
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\ResetFormAction;
45
use SilverStripe\Forms\TabSet;
46
use SilverStripe\Forms\TextField;
47
use SilverStripe\ORM\ArrayList;
48
use SilverStripe\ORM\DataList;
49
use SilverStripe\ORM\DataObject;
50
use SilverStripe\ORM\DB;
51
use SilverStripe\ORM\FieldType\DBHTMLText;
52
use SilverStripe\ORM\HiddenClass;
53
use SilverStripe\ORM\SS_List;
54
use SilverStripe\ORM\ValidationResult;
55
use SilverStripe\SiteConfig\SiteConfig;
56
use SilverStripe\Versioned\Versioned;
57
use SilverStripe\Security\Member;
58
use SilverStripe\Security\Permission;
59
use SilverStripe\Security\PermissionProvider;
60
use SilverStripe\Security\Security;
61
use SilverStripe\Security\SecurityToken;
62
use SilverStripe\View\ArrayData;
63
use SilverStripe\View\Requirements;
64
use Translatable;
65
use InvalidArgumentException;
66
use SilverStripe\Versioned\ChangeSet;
67
use SilverStripe\Versioned\ChangeSetItem;
68
69
/**
70
 * The main "content" area of the CMS.
71
 *
72
 * This class creates a 2-frame layout - left-tree and right-form - to sit beneath the main
73
 * admin menu.
74
 *
75
 * @todo Create some base classes to contain the generic functionality that will be replicated.
76
 *
77
 * @mixin LeftAndMainPageIconsExtension
78
 */
79
class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionProvider
80
{
81
82
    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...
83
84
    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...
85
86
    // Maintain a lower priority than other administration sections
87
    // so that Director does not think they are actions of CMSMain
88
    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...
89
90
    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...
91
92
    private static $menu_icon_class = 'font-icon-sitemap';
93
94
    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...
95
96
    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...
97
98
    private static $subitem_class = Member::class;
99
100
    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...
101
102
    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...
103
104
    /**
105
     * Amount of results showing on a single page.
106
     *
107
     * @config
108
     * @var int
109
     */
110
    private static $page_length = 15;
111
112
    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...
113
        'archive',
114
        'deleteitems',
115
        'DeleteItemsForm',
116
        'dialog',
117
        'duplicate',
118
        'duplicatewithchildren',
119
        'publishall',
120
        'publishitems',
121
        'PublishItemsForm',
122
        'submit',
123
        'EditForm',
124
        'SearchForm',
125
        'SiteTreeAsUL',
126
        'getshowdeletedsubtree',
127
        'savetreenode',
128
        'getsubtree',
129
        'updatetreenodes',
130
        'batchactions',
131
        'treeview',
132
        'listview',
133
        'ListViewForm',
134
        'childfilter',
135
    );
136
137
    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...
138
        'EditForm/$ID' => 'EditForm',
139
    ];
140
141
    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...
142
        'TreeIsFiltered' => 'Boolean',
143
        'AddForm' => 'HTMLFragment',
144
        'LinkPages' => 'Text',
145
        'Link' => 'Text',
146
        'ListViewForm' => 'HTMLFragment',
147
        'ExtraTreeTools' => 'HTMLFragment',
148
        'PageList' => 'HTMLFragment',
149
        'PageListSidebar' => 'HTMLFragment',
150
        'SiteTreeHints' => 'HTMLFragment',
151
        'SecurityID' => 'Text',
152
        'SiteTreeAsUL' => 'HTMLFragment',
153
    );
154
155
    protected function init()
156
    {
157
        // set reading lang
158
        if (SiteTree::has_extension('Translatable') && !$this->getRequest()->isAjax()) {
159
            Translatable::choose_site_locale(array_keys(Translatable::get_existing_content_languages('SilverStripe\\CMS\\Model\\SiteTree')));
160
        }
161
162
        parent::init();
163
164
        Requirements::javascript(CMS_DIR . '/client/dist/js/bundle.js');
165
        Requirements::javascript(CMS_DIR . '/client/dist/js/SilverStripeNavigator.js');
166
        Requirements::css(CMS_DIR . '/client/dist/styles/bundle.css');
167
        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...
168
        Requirements::add_i18n_javascript(CMS_DIR . '/client/lang', false, true);
169
170
        CMSBatchActionHandler::register('restore', CMSBatchAction_Restore::class);
171
        CMSBatchActionHandler::register('archive', CMSBatchAction_Archive::class);
172
        CMSBatchActionHandler::register('unpublish', CMSBatchAction_Unpublish::class);
173
        CMSBatchActionHandler::register('publish', CMSBatchAction_Publish::class);
174
    }
175
176
    public function index($request)
177
    {
178
        // In case we're not showing a specific record, explicitly remove any session state,
179
        // to avoid it being highlighted in the tree, and causing an edit form to show.
180
        if (!$request->param('Action')) {
181
            $this->setCurrentPageID(null);
182
        }
183
184
        return parent::index($request);
185
    }
186
187
    public function getResponseNegotiator()
188
    {
189
        $negotiator = parent::getResponseNegotiator();
190
191
        // ListViewForm
192
        $negotiator->setCallback('ListViewForm', function () {
193
            return $this->ListViewForm()->forTemplate();
194
        });
195
196
        // PageList view
197
        $negotiator->setCallback('Content-PageList', function () {
198
            return $this->PageList()->forTemplate();
199
        });
200
201
        // PageList view for edit controller
202
        $negotiator->setCallback('Content-PageList-Sidebar', function () {
203
            return $this->PageListSidebar()->forTemplate();
204
        });
205
206
        return $negotiator;
207
    }
208
209
    /**
210
     * Get pages listing area
211
     *
212
     * @return DBHTMLText
213
     */
214
    public function PageList()
215
    {
216
        return $this->renderWith($this->getTemplatesWithSuffix('_PageList'));
217
    }
218
219
    /**
220
     * Page list view for edit-form
221
     *
222
     * @return DBHTMLText
223
     */
224
    public function PageListSidebar()
225
    {
226
        return $this->renderWith($this->getTemplatesWithSuffix('_PageList_Sidebar'));
227
    }
228
229
    /**
230
     * If this is set to true, the "switchView" context in the
231
     * template is shown, with links to the staging and publish site.
232
     *
233
     * @return boolean
234
     */
235
    public function ShowSwitchView()
236
    {
237
        return true;
238
    }
239
240
    /**
241
     * Overloads the LeftAndMain::ShowView. Allows to pass a page as a parameter, so we are able
242
     * to switch view also for archived versions.
243
     *
244
     * @param SiteTree $page
245
     * @return array
246
     */
247
    public function SwitchView($page = null)
248
    {
249
        if (!$page) {
250
            $page = $this->currentPage();
251
        }
252
253
        if ($page) {
254
            $nav = SilverStripeNavigator::get_for_record($page);
255
            return $nav['items'];
256
        }
257
    }
258
259
    //------------------------------------------------------------------------------------------//
260
    // Main controllers
261
262
    //------------------------------------------------------------------------------------------//
263
    // Main UI components
264
265
    /**
266
     * Override {@link LeftAndMain} Link to allow blank URL segment for CMSMain.
267
     *
268
     * @param string|null $action Action to link to.
269
     * @return string
270
     */
271
    public function Link($action = null)
272
    {
273
        $link = Controller::join_links(
274
            AdminRootController::admin_url(),
275
            $this->stat('url_segment'), // in case we want to change the segment
276
            '/', // trailing slash needed if $action is null!
277
            "$action"
278
        );
279
        $this->extend('updateLink', $link);
280
        return $link;
281
    }
282
283
    public function LinkPages()
284
    {
285
        return CMSPagesController::singleton()->Link();
286
    }
287
288
    public function LinkPagesWithSearch()
289
    {
290
        return $this->LinkWithSearch($this->LinkPages());
291
    }
292
293
    /**
294
     * Get link to tree view
295
     *
296
     * @return string
297
     */
298
    public function LinkTreeView()
299
    {
300
        // Tree view is just default link to main pages section (no /treeview suffix)
301
        return $this->LinkWithSearch(CMSMain::singleton()->Link());
302
    }
303
304
    /**
305
     * Get link to list view
306
     *
307
     * @return string
308
     */
309
    public function LinkListView()
310
    {
311
        // Note : Force redirect to top level page controller
312
        return $this->LinkWithSearch(CMSMain::singleton()->Link('listview'));
313
    }
314
315 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...
316
    {
317
        if (!$id) {
318
            $id = $this->currentPageID();
319
        }
320
        return $this->LinkWithSearch(
321
            Controller::join_links(CMSPageEditController::singleton()->Link('show'), $id)
322
        );
323
    }
324
325 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...
326
    {
327
        if ($id = $this->currentPageID()) {
328
            return $this->LinkWithSearch(
329
                Controller::join_links(CMSPageSettingsController::singleton()->Link('show'), $id)
330
            );
331
        } else {
332
            return null;
333
        }
334
    }
335
336 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...
337
    {
338
        if ($id = $this->currentPageID()) {
339
            return $this->LinkWithSearch(
340
                Controller::join_links(CMSPageHistoryController::singleton()->Link('show'), $id)
341
            );
342
        } else {
343
            return null;
344
        }
345
    }
346
347
    public function LinkWithSearch($link)
348
    {
349
        // Whitelist to avoid side effects
350
        $params = array(
351
            'q' => (array)$this->getRequest()->getVar('q'),
352
            'ParentID' => $this->getRequest()->getVar('ParentID')
353
        );
354
        $link = Controller::join_links(
355
            $link,
356
            array_filter(array_values($params)) ? '?' . http_build_query($params) : null
357
        );
358
        $this->extend('updateLinkWithSearch', $link);
359
        return $link;
360
    }
361
362
    public function LinkPageAdd($extra = null, $placeholders = null)
363
    {
364
        $link = CMSPageAddController::singleton()->Link();
365
        $this->extend('updateLinkPageAdd', $link);
366
367
        if ($extra) {
368
            $link = Controller::join_links($link, $extra);
369
        }
370
371
        if ($placeholders) {
372
            $link .= (strpos($link, '?') === false ? "?$placeholders" : "&$placeholders");
373
        }
374
375
        return $link;
376
    }
377
378
    /**
379
     * @return string
380
     */
381
    public function LinkPreview()
382
    {
383
        $record = $this->getRecord($this->currentPageID());
384
        $baseLink = Director::absoluteBaseURL();
385
        if ($record && $record instanceof SiteTree) {
386
            // if we are an external redirector don't show a link
387
            if ($record instanceof RedirectorPage && $record->RedirectionType == 'External') {
388
                $baseLink = false;
389
            } else {
390
                $baseLink = $record->Link('?stage=Stage');
391
            }
392
        }
393
        return $baseLink;
394
    }
395
396
    /**
397
     * Return the entire site tree as a nested set of ULs
398
     */
399
    public function SiteTreeAsUL()
400
    {
401
        // Pre-cache sitetree version numbers for querying efficiency
402
        Versioned::prepopulate_versionnumber_cache(SiteTree::class, Versioned::DRAFT);
403
        Versioned::prepopulate_versionnumber_cache(SiteTree::class, Versioned::LIVE);
404
        $html = $this->getSiteTreeFor($this->stat('tree_class'));
405
406
        $this->extend('updateSiteTreeAsUL', $html);
407
408
        return $html;
409
    }
410
411
    /**
412
     * Get a site tree HTML listing which displays the nodes under the given criteria.
413
     *
414
     * @param string $className The class of the root object
415
     * @param string $rootID The ID of the root object.  If this is null then a complete tree will be
416
     *  shown
417
     * @param string $childrenMethod The method to call to get the children of the tree. For example,
418
     *  Children, AllChildrenIncludingDeleted, or AllHistoricalChildren
419
     * @param string $numChildrenMethod
420
     * @param callable $filterFunction
421
     * @param int $nodeCountThreshold
422
     * @return string Nested unordered list with links to each page
423
     */
424
    public function getSiteTreeFor(
425
        $className,
426
        $rootID = null,
427
        $childrenMethod = null,
428
        $numChildrenMethod = null,
429
        $filterFunction = null,
430
        $nodeCountThreshold = 30
431
    ) {
432
433
        // Filter criteria
434
        $filter = $this->getSearchFilter();
435
436
        // Default childrenMethod and numChildrenMethod
437
        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...
438
            $childrenMethod = ($filter && $filter->getChildrenMethod())
439
            ? $filter->getChildrenMethod()
440
            : 'AllChildrenIncludingDeleted';
441
        }
442
443
        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...
444
            $numChildrenMethod = 'numChildren';
445
            if ($filter && $filter->getNumChildrenMethod()) {
446
                $numChildrenMethod = $filter->getNumChildrenMethod();
447
            }
448
        }
449
        if (!$filterFunction && $filter) {
450
            $filterFunction = function ($node) use ($filter) {
451
                return $filter->isPageIncluded($node);
452
            };
453
        }
454
455
        // Get the tree root
456
        $record = ($rootID) ? $this->getRecord($rootID) : null;
457
        $obj = $record ? $record : singleton($className);
458
459
        // Get the current page
460
        // NOTE: This *must* be fetched before markPartialTree() is called, as this
461
        // causes the Hierarchy::$marked cache to be flushed (@see CMSMain::getRecord)
462
        // which means that deleted pages stored in the marked tree would be removed
463
        $currentPage = $this->currentPage();
464
465
        // Mark the nodes of the tree to return
466
        if ($filterFunction) {
467
            $obj->setMarkingFilterFunction($filterFunction);
468
        }
469
470
        $obj->markPartialTree($nodeCountThreshold, $this, $childrenMethod, $numChildrenMethod);
471
472
        // Ensure current page is exposed
473
        if ($currentPage) {
474
            $obj->markToExpose($currentPage);
475
        }
476
477
        SiteTree::prepopulate_permission_cache(
478
            'CanEditType',
479
            $obj->markedNodeIDs(),
480
            [ SiteTree::class, 'can_edit_multiple']
481
        );
482
483
        // getChildrenAsUL is a flexible and complex way of traversing the tree
484
        $controller = $this;
485
        $recordController = CMSPageEditController::singleton();
486
        $titleFn = function (&$child, $numChildrenMethod) use (&$controller, &$recordController, $filter) {
487
            $link = Controller::join_links($recordController->Link("show"), $child->ID);
488
            $node = CMSTreeNode::create($child, $link, $controller->isCurrentPage($child), $numChildrenMethod, $filter);
489
            return $node->forTemplate();
490
        };
491
492
        // Limit the amount of nodes shown for performance reasons.
493
        // Skip the check if we're filtering the tree, since its not clear how many children will
494
        // match the filter criteria until they're queried (and matched up with previously marked nodes).
495
        $nodeThresholdLeaf = SiteTree::config()->get('node_threshold_leaf');
496
        if ($nodeThresholdLeaf && !$filterFunction) {
497
            $nodeCountCallback = function ($parent, $numChildren) use (&$controller, $nodeThresholdLeaf) {
498
                if ( !$parent->ID || $numChildren <= $nodeThresholdLeaf) {
499
                    return null;
500
                }
501
                return sprintf(
502
                    '<ul><li class="readonly"><span class="item">'
503
                        . '%s (<a href="%s" class="cms-panel-link" data-pjax-target="Content">%s</a>)'
504
                        . '</span></li></ul>',
505
                    _t('LeftAndMain.TooManyPages', 'Too many pages'),
506
                    Controller::join_links(
507
                        $controller->LinkWithSearch($controller->Link()),
508
                        '?view=listview&ParentID=' . $parent->ID
509
                    ),
510
                    _t(
511
                        'LeftAndMain.ShowAsList',
512
                        'show as list',
513
                        'Show large amount of pages in list instead of tree view'
514
                    )
515
                );
516
            };
517
        } else {
518
            $nodeCountCallback = null;
519
        }
520
521
        // If the amount of pages exceeds the node thresholds set, use the callback
522
        $html = null;
523
        if ($obj->ParentID && $nodeCountCallback) {
524
            $html = $nodeCountCallback($obj, $obj->$numChildrenMethod());
525
        }
526
527
        // Otherwise return the actual tree (which might still filter leaf thresholds on children)
528
        if (!$html) {
529
            $html = $obj->getChildrenAsUL(
530
                "",
531
                $titleFn,
532
                CMSPagesController::singleton(),
533
                true,
534
                $childrenMethod,
535
                $numChildrenMethod,
536
                $nodeCountThreshold,
537
                $nodeCountCallback
538
            );
539
        }
540
541
        // Wrap the root if needs be.
542
        if (!$rootID) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $rootID 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...
543
            // This lets us override the tree title with an extension
544
            if ($this->hasMethod('getCMSTreeTitle') && $customTreeTitle = $this->getCMSTreeTitle()) {
0 ignored issues
show
Documentation Bug introduced by
The method getCMSTreeTitle 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...
545
                $treeTitle = $customTreeTitle;
546
            } elseif (class_exists(SiteConfig::class)) {
547
                $siteConfig = SiteConfig::current_site_config();
548
                $treeTitle =  Convert::raw2xml($siteConfig->Title);
549
            } else {
550
                $treeTitle = '...';
551
            }
552
553
            $html = "<ul><li id=\"record-0\" data-id=\"0\" class=\"Root nodelete\"><strong>$treeTitle</strong>"
554
                . $html . "</li></ul>";
555
        }
556
557
        return $html;
558
    }
559
560
    /**
561
     * Get a subtree underneath the request param 'ID'.
562
     * If ID = 0, then get the whole tree.
563
     *
564
     * @param HTTPRequest $request
565
     * @return string
566
     */
567
    public function getsubtree($request)
568
    {
569
        $html = $this->getSiteTreeFor(
570
            $this->stat('tree_class'),
571
            $request->getVar('ID'),
572
            null,
573
            null,
574
            null,
575
            $request->getVar('minNodeCount')
576
        );
577
578
        // Trim off the outer tag
579
        $html = preg_replace('/^[\s\t\r\n]*<ul[^>]*>/', '', $html);
580
        $html = preg_replace('/<\/ul[^>]*>[\s\t\r\n]*$/', '', $html);
581
582
        return $html;
583
    }
584
585
    /**
586
     * Allows requesting a view update on specific tree nodes.
587
     * Similar to {@link getsubtree()}, but doesn't enforce loading
588
     * all children with the node. Useful to refresh views after
589
     * state modifications, e.g. saving a form.
590
     *
591
     * @param HTTPRequest $request
592
     * @return HTTPResponse
593
     */
594
    public function updatetreenodes($request)
595
    {
596
        $data = array();
597
        $ids = explode(',', $request->getVar('ids'));
598
        foreach ($ids as $id) {
599
            if ($id === "") {
600
                continue; // $id may be a blank string, which is invalid and should be skipped over
601
            }
602
603
            $record = $this->getRecord($id);
604
            if (!$record) {
605
                continue; // In case a page is no longer available
606
            }
607
            $recordController = CMSPageEditController::singleton();
608
609
            // Find the next & previous nodes, for proper positioning (Sort isn't good enough - it's not a raw offset)
610
            // TODO: These methods should really be in hierarchy - for a start it assumes Sort exists
611
            $prev = null;
612
613
            $className = $this->stat('tree_class');
614
            $next = DataObject::get($className)
615
                ->filter('ParentID', $record->ParentID)
616
                ->filter('Sort:GreaterThan', $record->Sort)
617
                ->first();
618
619
            if (!$next) {
620
                $prev = DataObject::get($className)
621
                    ->filter('ParentID', $record->ParentID)
622
                    ->filter('Sort:LessThan', $record->Sort)
623
                    ->reverse()
624
                    ->first();
625
            }
626
627
            $link = Controller::join_links($recordController->Link("show"), $record->ID);
628
            $html = CMSTreeNode::create($record, $link, $this->isCurrentPage($record))->forTemplate(). '</li>';
629
630
            $data[$id] = array(
631
                'html' => $html,
632
                'ParentID' => $record->ParentID,
633
                'NextID' => $next ? $next->ID : null,
634
                'PrevID' => $prev ? $prev->ID : null
635
            );
636
        }
637
        return $this
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $this->getRespons...vert::raw2json($data)); (SilverStripe\Control\HTTPResponse) is incompatible with the return type of the parent method SilverStripe\Admin\LeftAndMain::updatetreenodes of type string.

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

Let’s take a look at an example:

class Author {
    private $name;

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

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

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

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

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

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

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

Loading history...
638
            ->getResponse()
639
            ->addHeader('Content-Type', 'application/json')
640
            ->setBody(Convert::raw2json($data));
641
    }
642
643
    /**
644
     * Update the position and parent of a tree node.
645
     * Only saves the node if changes were made.
646
     *
647
     * Required data:
648
     * - 'ID': The moved node
649
     * - 'ParentID': New parent relation of the moved node (0 for root)
650
     * - 'SiblingIDs': Array of all sibling nodes to the moved node (incl. the node itself).
651
     *   In case of a 'ParentID' change, relates to the new siblings under the new parent.
652
     *
653
     * @param HTTPRequest $request
654
     * @return HTTPResponse JSON string with a
655
     * @throws HTTPResponse_Exception
656
     */
657
    public function savetreenode($request)
658
    {
659
        if (!SecurityToken::inst()->checkRequest($request)) {
660
            return $this->httpError(400);
661
        }
662
        if (!Permission::check('SITETREE_REORGANISE') && !Permission::check('ADMIN')) {
663
            return $this->httpError(
664
                403,
665
                _t(
666
                    'LeftAndMain.CANT_REORGANISE',
667
                    "You do not have permission to rearange the site tree. Your change was not saved."
668
                )
669
            );
670
        }
671
672
        $className = $this->stat('tree_class');
673
        $id = $request->requestVar('ID');
674
        $parentID = $request->requestVar('ParentID');
675
        if (!is_numeric($id) || !is_numeric($parentID)) {
676
            return $this->httpError(400);
677
        }
678
679
        // Check record exists in the DB
680
        /** @var SiteTree $node */
681
        $node = DataObject::get_by_id($className, $id);
682
        if (!$node) {
683
            return $this->httpError(
684
                500,
685
                _t(
686
                    'LeftAndMain.PLEASESAVE',
687
                    "Please Save Page: This page could not be updated because it hasn't been saved yet."
688
                )
689
            );
690
        }
691
692
        // Check top level permissions
693
        $root = $node->getParentType();
694
        if (($parentID == '0' || $root == 'root') && !SiteConfig::current_site_config()->canCreateTopLevel()) {
695
            return $this->httpError(
696
                403,
697
                _t(
698
                    'LeftAndMain.CANT_REORGANISE',
699
                    "You do not have permission to alter Top level pages. Your change was not saved."
700
                )
701
            );
702
        }
703
704
        $siblingIDs = $request->requestVar('SiblingIDs');
705
        $statusUpdates = array('modified'=>array());
706
707
        if (!$node->canEdit()) {
708
            return Security::permissionFailure($this);
709
        }
710
711
        // Update hierarchy (only if ParentID changed)
712
        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...
713
            $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...
714
            $node->write();
715
716
            $statusUpdates['modified'][$node->ID] = array(
717
                '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...
718
            );
719
720
            // Update all dependent pages
721
            $virtualPages = VirtualPage::get()->filter("CopyContentFromID", $node->ID);
722
            foreach ($virtualPages as $virtualPage) {
723
                $statusUpdates['modified'][$virtualPage->ID] = array(
724
                    'TreeTitle' => $virtualPage->TreeTitle()
725
                );
726
            }
727
728
            $this->getResponse()->addHeader(
729
                'X-Status',
730
                rawurlencode(_t('LeftAndMain.REORGANISATIONSUCCESSFUL', 'Reorganised the site tree successfully.'))
731
            );
732
        }
733
734
        // Update sorting
735
        if (is_array($siblingIDs)) {
736
            $counter = 0;
737
            foreach ($siblingIDs as $id) {
738
                if ($id == $node->ID) {
739
                    $node->Sort = ++$counter;
740
                    $node->write();
741
                    $statusUpdates['modified'][$node->ID] = array(
742
                        '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...
743
                    );
744
                } elseif (is_numeric($id)) {
745
                    // Nodes that weren't "actually moved" shouldn't be registered as
746
                    // having been edited; do a direct SQL update instead
747
                    ++$counter;
748
                    $table = DataObject::getSchema()->baseDataTable($className);
749
                    DB::prepared_query(
750
                        "UPDATE \"$table\" SET \"Sort\" = ? WHERE \"ID\" = ?",
751
                        array($counter, $id)
752
                    );
753
                }
754
            }
755
756
            $this->getResponse()->addHeader(
757
                'X-Status',
758
                rawurlencode(_t('LeftAndMain.REORGANISATIONSUCCESSFUL', 'Reorganised the site tree successfully.'))
759
            );
760
        }
761
762
        return $this
763
            ->getResponse()
764
            ->addHeader('Content-Type', 'application/json')
765
            ->setBody(Convert::raw2json($statusUpdates));
766
    }
767
768
    public function CanOrganiseSitetree()
769
    {
770
        return !Permission::check('SITETREE_REORGANISE') && !Permission::check('ADMIN') ? false : true;
771
    }
772
773
    /**
774
     * @return boolean
775
     */
776
    public function TreeIsFiltered()
777
    {
778
        $query = $this->getRequest()->getVar('q');
779
780
        if (!$query || (count($query) === 1 && isset($query['FilterClass']) && $query['FilterClass'] === 'SilverStripe\\CMS\\Controllers\\CMSSiteTreeFilter_Search')) {
781
            return false;
782
        }
783
784
        return true;
785
    }
786
787
    public function ExtraTreeTools()
788
    {
789
        $html = '';
790
        $this->extend('updateExtraTreeTools', $html);
791
        return $html;
792
    }
793
794
    /**
795
     * Returns a Form for page searching for use in templates.
796
     *
797
     * Can be modified from a decorator by a 'updateSearchForm' method
798
     *
799
     * @return Form
800
     */
801
    public function SearchForm()
802
    {
803
        // Create the fields
804
        $content = new TextField('q[Term]', _t('CMSSearch.FILTERLABELTEXT', 'Search'));
805
        $dateFrom = new DateField(
806
            'q[LastEditedFrom]',
807
            _t('CMSSearch.FILTERDATEFROM', 'From')
808
        );
809
        $dateTo = new DateField(
810
            'q[LastEditedTo]',
811
            _t('CMSSearch.FILTERDATETO', 'To')
812
        );
813
        $pageFilter = new DropdownField(
814
            'q[FilterClass]',
815
            _t('CMSMain.PAGES', 'Page status'),
816
            CMSSiteTreeFilter::get_all_filters()
817
        );
818
        $pageClasses = new DropdownField(
819
            'q[ClassName]',
820
            _t('CMSMain.PAGETYPEOPT', 'Page type', 'Dropdown for limiting search to a page type'),
821
            $this->getPageTypes()
822
        );
823
        $pageClasses->setEmptyString(_t('CMSMain.PAGETYPEANYOPT', 'Any'));
824
825
        // Group the Datefields
826
        $dateGroup = new FieldGroup(
827
            $dateFrom,
828
            $dateTo
829
        );
830
        $dateGroup->setTitle(_t('CMSSearch.PAGEFILTERDATEHEADING', 'Last edited'));
831
832
        // view mode
833
        $viewMode = HiddenField::create('view', false, $this->ViewState());
834
835
        // Create the Field list
836
        $fields = new FieldList(
837
            $content,
838
            $pageFilter,
839
            $pageClasses,
840
            $dateGroup,
841
            $viewMode
842
        );
843
844
        // Create the Search and Reset action
845
        $actions = new FieldList(
846
            FormAction::create('doSearch', _t('CMSMain_left_ss.APPLY_FILTER', 'Search'))
847
                ->addExtraClass('btn btn-primary'),
848
            ResetFormAction::create('clear', _t('CMSMain_left_ss.CLEAR_FILTER', 'Clear'))
849
                ->addExtraClass('btn btn-secondary')
850
        );
851
852
        // Use <button> to allow full jQuery UI styling on the all of the Actions
853
        /** @var FormAction $action */
854
        foreach ($actions->dataFields() as $action) {
855
            /** @var FormAction $action */
856
            $action->setUseButtonTag(true);
857
        }
858
859
        // Create the form
860
        /** @skipUpgrade */
861
        $form = Form::create($this, 'SearchForm', $fields, $actions)
862
            ->addExtraClass('cms-search-form')
863
            ->setFormMethod('GET')
864
            ->setFormAction($this->Link())
865
            ->disableSecurityToken()
866
            ->unsetValidator();
867
868
        // Load the form with previously sent search data
869
        $form->loadDataFrom($this->getRequest()->getVars());
870
871
        // Allow decorators to modify the form
872
        $this->extend('updateSearchForm', $form);
873
874
        return $form;
875
    }
876
877
    /**
878
     * Returns a sorted array suitable for a dropdown with pagetypes and their translated name
879
     *
880
     * @return array
881
     */
882
    protected function getPageTypes()
883
    {
884
        $pageTypes = array();
885
        foreach (SiteTree::page_type_classes() as $pageTypeClass) {
886
            $pageTypes[$pageTypeClass] = SiteTree::singleton($pageTypeClass)->i18n_singular_name();
887
        }
888
        asort($pageTypes);
889
        return $pageTypes;
890
    }
891
892
    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...
893
    {
894
        return $this->getsubtree($this->getRequest());
895
    }
896
897
    /**
898
     * @param bool $unlinked
899
     * @return ArrayList
900
     */
901
    public function Breadcrumbs($unlinked = false)
902
    {
903
        $items = parent::Breadcrumbs($unlinked);
904
905
        if ($items->count() > 1) {
906
            // Specific to the SiteTree admin section, we never show the cms section and current
907
            // page in the same breadcrumbs block.
908
            $items->shift();
909
        }
910
911
        return $items;
912
    }
913
914
    /**
915
     * Create serialized JSON string with site tree hints data to be injected into
916
     * 'data-hints' attribute of root node of jsTree.
917
     *
918
     * @return string Serialized JSON
919
     */
920
    public function SiteTreeHints()
921
    {
922
        $classes = SiteTree::page_type_classes();
923
924
        $cacheCanCreate = array();
925
        foreach ($classes as $class) {
926
            $cacheCanCreate[$class] = singleton($class)->canCreate();
927
        }
928
929
        // Generate basic cache key. Too complex to encompass all variations
930
        $cache = Injector::inst()->get(CacheInterface::class . '.CMSMain_SiteTreeHints');
931
        $cacheKey = md5(implode('_', array(Member::currentUserID(), implode(',', $cacheCanCreate), implode(',', $classes))));
932
        if ($this->getRequest()->getVar('flush')) {
933
            $cache->clear();
934
        }
935
        $json = $cache->get($cacheKey);
936
        if (!$json) {
937
            $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...
938
            $def['Root']['disallowedChildren'] = array();
939
940
            // Contains all possible classes to support UI controls listing them all,
941
            // such as the "add page here" context menu.
942
            $def['All'] = array();
943
944
            // Identify disallows and set globals
945
            foreach ($classes as $class) {
946
                $obj = singleton($class);
947
                if ($obj instanceof HiddenClass) {
948
                    continue;
949
                }
950
951
                // Name item
952
                $def['All'][$class] = array(
953
                    'title' => $obj->i18n_singular_name()
954
                );
955
956
                // Check if can be created at the root
957
                $needsPerm = $obj->stat('need_permission');
958
                if (!$obj->stat('can_be_root')
959
                    || (!array_key_exists($class, $cacheCanCreate) || !$cacheCanCreate[$class])
960
                    || ($needsPerm && !$this->can($needsPerm))
961
                ) {
962
                    $def['Root']['disallowedChildren'][] = $class;
963
                }
964
965
                // Hint data specific to the class
966
                $def[$class] = array();
967
968
                $defaultChild = $obj->defaultChild();
969
                if ($defaultChild !== 'Page' && $defaultChild !== null) {
970
                    $def[$class]['defaultChild'] = $defaultChild;
971
                }
972
973
                $defaultParent = $obj->defaultParent();
974
                if ($defaultParent !== 1 && $defaultParent !== null) {
975
                    $def[$class]['defaultParent'] = $defaultParent;
976
                }
977
            }
978
979
            $this->extend('updateSiteTreeHints', $def);
980
981
            $json = Convert::raw2json($def);
982
            $cache->set($cacheKey, $json);
983
        }
984
        return $json;
985
    }
986
987
    /**
988
     * Populates an array of classes in the CMS
989
     * which allows the user to change the page type.
990
     *
991
     * @return SS_List
992
     */
993
    public function PageTypes()
994
    {
995
        $classes = SiteTree::page_type_classes();
996
997
        $result = new ArrayList();
998
999
        foreach ($classes as $class) {
1000
            $instance = SiteTree::singleton($class);
1001
            if ($instance instanceof HiddenClass) {
1002
                continue;
1003
            }
1004
1005
            // skip this type if it is restricted
1006
            if ($instance->stat('need_permission') && !$this->can(singleton($class)->stat('need_permission'))) {
1007
                continue;
1008
            }
1009
1010
            $singularName = $instance->i18n_singular_name();
1011
            $description = $instance->i18n_classDescription();
1012
1013
            $result->push(new ArrayData(array(
1014
                'ClassName' => $class,
1015
                'AddAction' => $singularName,
1016
                'Description' => $description,
1017
                // TODO Sprite support
1018
                'IconURL' => $instance->stat('icon'),
1019
                'Title' => $singularName,
1020
            )));
1021
        }
1022
1023
        $result = $result->sort('AddAction');
1024
1025
        return $result;
1026
    }
1027
1028
    /**
1029
     * Get a database record to be managed by the CMS.
1030
     *
1031
     * @param int $id Record ID
1032
     * @param int $versionID optional Version id of the given record
1033
     * @return SiteTree
1034
     */
1035
    public function getRecord($id, $versionID = null)
1036
    {
1037
        if (!$id) {
1038
            return null;
1039
        }
1040
        $treeClass = $this->stat('tree_class');
1041
        if ($id instanceof $treeClass) {
1042
            return $id;
1043
        }
1044
        if (substr($id, 0, 3) == 'new') {
1045
            return $this->getNewItem($id);
1046
        }
1047
        if (!is_numeric($id)) {
1048
            return null;
1049
        }
1050
1051
        $currentStage = Versioned::get_reading_mode();
1052
1053
        if ($this->getRequest()->getVar('Version')) {
1054
            $versionID = (int) $this->getRequest()->getVar('Version');
1055
        }
1056
1057
        /** @var SiteTree $record */
1058
        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...
1059
            $record = Versioned::get_version($treeClass, $id, $versionID);
1060
        } else {
1061
            $record = DataObject::get_by_id($treeClass, $id);
1062
        }
1063
1064
        // Then, try getting a record from the live site
1065
        if (!$record) {
1066
            // $record = Versioned::get_one_by_stage($treeClass, "Live", "\"$treeClass\".\"ID\" = $id");
1067
            Versioned::set_stage(Versioned::LIVE);
1068
            singleton($treeClass)->flushCache();
1069
1070
            $record = DataObject::get_by_id($treeClass, $id);
1071
        }
1072
1073
        // Then, try getting a deleted record
1074
        if (!$record) {
1075
            $record = Versioned::get_latest_version($treeClass, $id);
1076
        }
1077
1078
        // Set the reading mode back to what it was.
1079
        Versioned::set_reading_mode($currentStage);
1080
1081
        return $record;
1082
    }
1083
1084
    /**
1085
     * {@inheritdoc}
1086
     *
1087
     * @param HTTPRequest $request
1088
     * @return Form
1089
     */
1090
    public function EditForm($request = null)
1091
    {
1092
        // set page ID from request
1093
        if ($request) {
1094
            // Validate id is present
1095
            $id = $request->param('ID');
1096
            if (!isset($id)) {
1097
                $this->httpError(400);
1098
                return null;
1099
            }
1100
            $this->setCurrentPageID($id);
1101
        }
1102
        return $this->getEditForm();
1103
    }
1104
1105
    /**
1106
     * @param int $id
1107
     * @param FieldList $fields
1108
     * @return Form
1109
     */
1110
    public function getEditForm($id = null, $fields = null)
1111
    {
1112
        // Get record
1113
        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...
1114
            $id = $this->currentPageID();
1115
        }
1116
        /** @var SiteTree $record */
1117
        $record = $this->getRecord($id);
1118
1119
        // Check parent form can be generated
1120
        $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...
1121
        if (!$form || !$record) {
1122
            return $form;
1123
        }
1124
1125
        if (!$fields) {
1126
            $fields = $form->Fields();
1127
        }
1128
1129
        // Add extra fields
1130
        $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...
1131
        $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...
1132
        // Necessary for different subsites
1133
        $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...
1134
        $fields->push($liveLinkField = new HiddenField("LiveLink"));
1135
        $fields->push($stageLinkField = new HiddenField("StageLink"));
1136
        $fields->push($archiveWarningMsgField = new HiddenField("ArchiveWarningMessage"));
1137
        $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...
1138
1139
        $archiveWarningMsgField->setValue($this->getArchiveWarningMessage($record));
1140
1141
        // Build preview / live links
1142
        $liveLink = $record->getAbsoluteLiveLink();
1143
        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...
1144
            $liveLinkField->setValue($liveLink);
1145
        }
1146
        if (!$deletedFromStage) {
1147
            $stageLink = Controller::join_links($record->AbsoluteLink(), '?stage=Stage');
1148
            if ($stageLink) {
1149
                $stageLinkField->setValue($stageLink);
1150
            }
1151
        }
1152
1153
        // Added in-line to the form, but plucked into different view by LeftAndMain.Preview.js upon load
1154
        /** @skipUpgrade */
1155
        if ($record instanceof CMSPreviewable && !$fields->fieldByName('SilverStripeNavigator')) {
1156
            $navField = new LiteralField('SilverStripeNavigator', $this->getSilverStripeNavigator());
1157
            $navField->setAllowHTML(true);
1158
            $fields->push($navField);
1159
        }
1160
1161
        // getAllCMSActions can be used to completely redefine the action list
1162
        if ($record->hasMethod('getAllCMSActions')) {
1163
            $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...
1164
        } else {
1165
            $actions = $record->getCMSActions();
1166
1167
            // Find and remove action menus that have no actions.
1168
            if ($actions && $actions->count()) {
1169
                /** @var TabSet $tabset */
1170
                $tabset = $actions->fieldByName('ActionMenus');
1171
                if ($tabset) {
1172
                    /** @var Tab $tab */
1173
                    foreach ($tabset->getChildren() as $tab) {
1174
                        if (!$tab->getChildren()->count()) {
1175
                            $tabset->removeByName($tab->getName());
1176
                        }
1177
                    }
1178
                }
1179
            }
1180
        }
1181
1182
        // Use <button> to allow full jQuery UI styling
1183
        $actionsFlattened = $actions->dataFields();
1184
        if ($actionsFlattened) {
1185
            /** @var FormAction $action */
1186
            foreach ($actionsFlattened as $action) {
1187
                $action->setUseButtonTag(true);
1188
            }
1189
        }
1190
1191
        // TODO Can't merge $FormAttributes in template at the moment
1192
        $form->addExtraClass('center ' . $this->BaseCSSClasses());
1193
        // Set validation exemptions for specific actions
1194
        $form->setValidationExemptActions(array('restore', 'revert', 'deletefromlive', 'delete', 'unpublish', 'rollback', 'doRollback'));
1195
1196
        // Announce the capability so the frontend can decide whether to allow preview or not.
1197
        if ($record instanceof CMSPreviewable) {
1198
            $form->addExtraClass('cms-previewable');
1199
        }
1200
        $form->addExtraClass('fill-height flexbox-area-grow');
1201
1202
        if (!$record->canEdit() || $deletedFromStage) {
1203
            $readonlyFields = $form->Fields()->makeReadonly();
1204
            $form->setFields($readonlyFields);
1205
        }
1206
1207
        $form->Fields()->setForm($form);
1208
1209
        $this->extend('updateEditForm', $form);
1210
1211
        // Use custom reqest handler for LeftAndMain requests;
1212
        // CMS Forms cannot be identified solely by name, but also need ID (and sometimes OtherID)
1213
        $form->setRequestHandler(
1214
            LeftAndMainFormRequestHandler::create($form, [$id])
1215
        );
1216
        return $form;
1217
    }
1218
1219
    public function EmptyForm()
1220
    {
1221
        $fields = new FieldList(
1222
            new LabelField('PageDoesntExistLabel', _t('CMSMain.PAGENOTEXISTS', "This page doesn't exist"))
1223
        );
1224
        $form = parent::EmptyForm();
1225
        $form->setFields($fields);
1226
        $fields->setForm($form);
1227
        return $form;
1228
    }
1229
1230
    /**
1231
     * Build an archive warning message based on the page's children
1232
     *
1233
     * @param SiteTree $record
1234
     * @return string
1235
     */
1236
    protected function getArchiveWarningMessage($record)
1237
    {
1238
        // Get all page's descendants
1239
        $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...
1240
        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...
1241
            $descendants = [];
1242
        }
1243
1244
        // Get all campaigns that the page and its descendants belong to
1245
        $inChangeSetIDs = ChangeSetItem::get_for_object($record)->column('ChangeSetID');
1246
1247
        foreach ($descendants as $page) {
1248
            $inChangeSetIDs = array_merge($inChangeSetIDs, ChangeSetItem::get_for_object($page)->column('ChangeSetID'));
1249
        }
1250
1251
        if (count($inChangeSetIDs) > 0) {
1252
            $inChangeSets = ChangeSet::get()->filter(['ID' => $inChangeSetIDs, 'State' => ChangeSet::STATE_OPEN]);
1253
        } else {
1254
            $inChangeSets = new ArrayList();
1255
        }
1256
1257
        $numCampaigns = ChangeSet::singleton()->i18n_pluralise($inChangeSets->count());
1258
        $numCampaigns = mb_strtolower($numCampaigns);
1259
1260
        if (count($descendants) > 0 && $inChangeSets->count() > 0) {
1261
            $archiveWarningMsg = _t('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 ]);
1262
        } elseif (count($descendants) > 0) {
1263
            $archiveWarningMsg = _t('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?');
1264
        } elseif ($inChangeSets->count() > 0) {
1265
            $archiveWarningMsg = _t('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 ]);
1266
        } else {
1267
            $archiveWarningMsg = _t('CMSMain.ArchiveWarning', 'Warning: This page will be unpublished before being sent to the archive.\n\nAre you sure you want to proceed?');
1268
        }
1269
1270
        return $archiveWarningMsg;
1271
    }
1272
1273
    /**
1274
     * @param HTTPRequest $request
1275
     * @return string HTML
1276
     */
1277
    public function treeview($request)
1278
    {
1279
        return $this->getResponseNegotiator()->respond($request);
1280
    }
1281
1282
    /**
1283
     * @param HTTPRequest $request
1284
     * @return string HTML
1285
     */
1286
    public function listview($request)
1287
    {
1288
        return $this->getResponseNegotiator()->respond($request);
1289
    }
1290
1291
    /**
1292
     * @return string
1293
     */
1294
    public function ViewState()
1295
    {
1296
        $mode = $this->getRequest()->requestVar('view')
1297
            ?: $this->getRequest()->param('Action');
1298
        switch ($mode) {
1299
            case 'listview':
1300
            case 'treeview':
1301
                return $mode;
1302
            default:
1303
                return 'treeview';
1304
        }
1305
    }
1306
1307
    /**
1308
     * Callback to request the list of page types allowed under a given page instance.
1309
     * Provides a slower but more precise response over SiteTreeHints
1310
     *
1311
     * @param HTTPRequest $request
1312
     * @return HTTPResponse
1313
     */
1314
    public function childfilter($request)
1315
    {
1316
        // Check valid parent specified
1317
        $parentID = $request->requestVar('ParentID');
1318
        $parent = SiteTree::get()->byID($parentID);
1319
        if (!$parent || !$parent->exists()) {
1320
            return $this->httpError(404);
1321
        }
1322
1323
        // Build hints specific to this class
1324
        // Identify disallows and set globals
1325
        $classes = SiteTree::page_type_classes();
1326
        $disallowedChildren = array();
1327
        foreach ($classes as $class) {
1328
            $obj = singleton($class);
1329
            if ($obj instanceof HiddenClass) {
1330
                continue;
1331
            }
1332
1333
            if (!$obj->canCreate(null, array('Parent' => $parent))) {
1334
                $disallowedChildren[] = $class;
1335
            }
1336
        }
1337
1338
        $this->extend('updateChildFilter', $disallowedChildren, $parentID);
1339
        return $this
1340
            ->getResponse()
1341
            ->addHeader('Content-Type', 'application/json; charset=utf-8')
1342
            ->setBody(Convert::raw2json($disallowedChildren));
1343
    }
1344
1345
    /**
1346
     * Safely reconstruct a selected filter from a given set of query parameters
1347
     *
1348
     * @param array $params Query parameters to use
1349
     * @return CMSSiteTreeFilter The filter class, or null if none present
1350
     * @throws InvalidArgumentException if invalid filter class is passed.
1351
     */
1352
    protected function getQueryFilter($params)
1353
    {
1354
        if (empty($params['FilterClass'])) {
1355
            return null;
1356
        }
1357
        $filterClass = $params['FilterClass'];
1358
        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...
1359
            throw new InvalidArgumentException("Invalid filter class passed: {$filterClass}");
1360
        }
1361
        return $filterClass::create($params);
1362
    }
1363
1364
    /**
1365
     * Returns the pages meet a certain criteria as {@see CMSSiteTreeFilter} or the subpages of a parent page
1366
     * defaulting to no filter and show all pages in first level.
1367
     * Doubles as search results, if any search parameters are set through {@link SearchForm()}.
1368
     *
1369
     * @param array $params Search filter criteria
1370
     * @param int $parentID Optional parent node to filter on (can't be combined with other search criteria)
1371
     * @return SS_List
1372
     * @throws InvalidArgumentException if invalid filter class is passed.
1373
     */
1374
    public function getList($params = array(), $parentID = 0)
1375
    {
1376
        if ($filter = $this->getQueryFilter($params)) {
1377
            return $filter->getFilteredPages();
1378
        } else {
1379
            $list = DataList::create($this->stat('tree_class'));
1380
            $parentID = is_numeric($parentID) ? $parentID : 0;
1381
            return $list->filter("ParentID", $parentID);
1382
        }
1383
    }
1384
1385
    /**
1386
     * @return Form
1387
     */
1388
    public function ListViewForm()
1389
    {
1390
        $params = $this->getRequest()->requestVar('q');
1391
        $list = $this->getList($params, $parentID = $this->getRequest()->requestVar('ParentID'));
1392
        $gridFieldConfig = GridFieldConfig::create()->addComponents(
1393
            new GridFieldSortableHeader(),
1394
            new GridFieldDataColumns(),
1395
            new GridFieldPaginator($this->config()->get('page_length'))
1396
        );
1397
        if ($parentID) {
1398
            $linkSpec = $this->Link();
1399
            $linkSpec = $linkSpec . (strstr($linkSpec, '?') ? '&' : '?') . 'ParentID=%d&view=listview';
1400
            $gridFieldConfig->addComponent(
1401
                GridFieldLevelup::create($parentID)
1402
                    ->setLinkSpec($linkSpec)
1403
                    ->setAttributes(array('data-pjax' => 'ListViewForm,Breadcrumbs'))
1404
            );
1405
        }
1406
        $gridField = new GridField('Page', 'Pages', $list, $gridFieldConfig);
1407
        /** @var GridFieldDataColumns $columns */
1408
        $columns = $gridField->getConfig()->getComponentByType('SilverStripe\\Forms\\GridField\\GridFieldDataColumns');
1409
1410
        // Don't allow navigating into children nodes on filtered lists
1411
        $fields = array(
1412
            'getTreeTitle' => _t('SiteTree.PAGETITLE', 'Page Title'),
1413
            'singular_name' => _t('SiteTree.PAGETYPE', 'Page Type'),
1414
            'LastEdited' => _t('SiteTree.LASTUPDATED', 'Last Updated'),
1415
        );
1416
        /** @var GridFieldSortableHeader $sortableHeader */
1417
        $sortableHeader = $gridField->getConfig()->getComponentByType('SilverStripe\\Forms\\GridField\\GridFieldSortableHeader');
1418
        $sortableHeader->setFieldSorting(array('getTreeTitle' => 'Title'));
1419
        $gridField->getState()->ParentID = $parentID;
1420
1421
        if (!$params) {
1422
            $fields = array_merge(array('listChildrenLink' => ''), $fields);
1423
        }
1424
1425
        $columns->setDisplayFields($fields);
1426
        $columns->setFieldCasting(array(
1427
            'Created' => 'DBDatetime->Ago',
1428
            'LastEdited' => 'DBDatetime->FormatFromSettings',
1429
            'getTreeTitle' => 'HTMLFragment'
1430
        ));
1431
1432
        $controller = $this;
1433
        $columns->setFieldFormatting(array(
1434
            'listChildrenLink' => function ($value, &$item) use ($controller) {
1435
                /** @var SiteTree $item */
1436
                $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...
1437
                if ($num) {
1438
                    return sprintf(
1439
                        '<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>',
1440
                        Controller::join_links(
1441
                            $controller->Link(),
1442
                            sprintf("?ParentID=%d&view=listview", (int)$item->ID)
1443
                        ),
1444
                        $num
1445
                    );
1446
                }
1447
            },
1448
            'getTreeTitle' => function ($value, &$item) use ($controller) {
1449
                return sprintf(
1450
                    '<a class="action-detail" href="%s">%s</a>',
1451
                    Controller::join_links(
1452
                        CMSPageEditController::singleton()->Link('show'),
1453
                        (int)$item->ID
1454
                    ),
1455
                    $item->TreeTitle // returns HTML, does its own escaping
1456
                );
1457
            }
1458
        ));
1459
1460
        $negotiator = $this->getResponseNegotiator();
1461
        $listview = Form::create(
1462
            $this,
1463
            'ListViewForm',
1464
            new FieldList($gridField),
1465
            new FieldList()
1466
        )->setHTMLID('Form_ListViewForm');
1467
        $listview->setAttribute('data-pjax-fragment', 'ListViewForm');
1468 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...
1469
            $request = $this->getRequest();
1470
            if ($request->isAjax() && $negotiator) {
1471
                $result = $listview->forTemplate();
1472
                return $negotiator->respond($request, array(
1473
                    'CurrentForm' => function () use ($result) {
1474
                        return $result;
1475
                    }
1476
                ));
1477
            }
1478
        });
1479
1480
        $this->extend('updateListView', $listview);
1481
1482
        $listview->disableSecurityToken();
1483
        return $listview;
1484
    }
1485
1486
    public function currentPageID()
1487
    {
1488
        $id = parent::currentPageID();
1489
1490
        $this->extend('updateCurrentPageID', $id);
1491
1492
        return $id;
1493
    }
1494
1495
    //------------------------------------------------------------------------------------------//
1496
    // Data saving handlers
1497
1498
    /**
1499
     * Save and Publish page handler
1500
     *
1501
     * @param array $data
1502
     * @param Form $form
1503
     * @return HTTPResponse
1504
     * @throws HTTPResponse_Exception
1505
     */
1506
    public function save($data, $form)
1507
    {
1508
        $className = $this->stat('tree_class');
1509
1510
        // Existing or new record?
1511
        $id = $data['ID'];
1512
        if (substr($id, 0, 3) != 'new') {
1513
            /** @var SiteTree $record */
1514
            $record = DataObject::get_by_id($className, $id);
1515
            // Check edit permissions
1516
            if ($record && !$record->canEdit()) {
1517
                return Security::permissionFailure($this);
1518
            }
1519
            if (!$record || !$record->ID) {
1520
                throw new HTTPResponse_Exception("Bad record ID #$id", 404);
1521
            }
1522
        } else {
1523
            if (!$className::singleton()->canCreate()) {
1524
                return Security::permissionFailure($this);
1525
            }
1526
            $record = $this->getNewItem($id, false);
1527
        }
1528
1529
        // Check publishing permissions
1530
        $doPublish = !empty($data['publish']);
1531
        if ($record && $doPublish && !$record->canPublish()) {
1532
            return Security::permissionFailure($this);
1533
        }
1534
1535
        // TODO Coupling to SiteTree
1536
        $record->HasBrokenLink = 0;
1537
        $record->HasBrokenFile = 0;
1538
1539
        if (!$record->ObsoleteClassName) {
1540
            $record->writeWithoutVersion();
1541
        }
1542
1543
        // Update the class instance if necessary
1544
        if (isset($data['ClassName']) && $data['ClassName'] != $record->ClassName) {
1545
            // Replace $record with a new instance of the new class
1546
            $newClassName = $data['ClassName'];
1547
            $record = $record->newClassInstance($newClassName);
1548
        }
1549
1550
        // save form data into record
1551
        $form->saveInto($record);
1552
        $record->write();
1553
1554
        // If the 'Save & Publish' button was clicked, also publish the page
1555
        if ($doPublish) {
1556
            $record->publishRecursive();
1557
            $message = _t(
1558
                'CMSMain.PUBLISHED',
1559
                "Published '{title}' successfully.",
1560
                ['title' => $record->Title]
1561
            );
1562
        } else {
1563
            $message = _t(
1564
                'CMSMain.SAVED',
1565
                "Saved '{title}' successfully.",
1566
                ['title' => $record->Title]
1567
            );
1568
        }
1569
1570
        $this->getResponse()->addHeader('X-Status', rawurlencode($message));
1571
        return $this->getResponseNegotiator()->respond($this->getRequest());
1572
    }
1573
1574
    /**
1575
     * @uses LeftAndMainExtension->augmentNewSiteTreeItem()
1576
     *
1577
     * @param int|string $id
1578
     * @param bool $setID
1579
     * @return mixed|DataObject
1580
     * @throws HTTPResponse_Exception
1581
     */
1582
    public function getNewItem($id, $setID = true)
1583
    {
1584
        $parentClass = $this->stat('tree_class');
1585
        list($dummy, $className, $parentID, $suffix) = array_pad(explode('-', $id), 4, null);
0 ignored issues
show
Unused Code introduced by
The assignment to $dummy is unused. Consider omitting it like so list($first,,$third).

This checks looks for assignemnts to variables using the list(...) function, where not all assigned variables are subsequently used.

Consider the following code example.

<?php

function returnThreeValues() {
    return array('a', 'b', 'c');
}

list($a, $b, $c) = returnThreeValues();

print $a . " - " . $c;

Only the variables $a and $c are used. There was no need to assign $b.

Instead, the list call could have been.

list($a,, $c) = returnThreeValues();
Loading history...
1586
1587
        if (!is_a($className, $parentClass, true)) {
1588
            $response = Security::permissionFailure($this);
1589
            if (!$response) {
1590
                $response = $this->getResponse();
1591
            }
1592
            throw new HTTPResponse_Exception($response);
1593
        }
1594
1595
        /** @var SiteTree $newItem */
1596
        $newItem = Injector::inst()->create($className);
1597
        if (!$suffix) {
1598
            $sessionTag = "NewItems." . $parentID . "." . $className;
1599
            if (Session::get($sessionTag)) {
1600
                $suffix = '-' . Session::get($sessionTag);
1601
                Session::set($sessionTag, Session::get($sessionTag) + 1);
1602
            } else {
1603
                Session::set($sessionTag, 1);
1604
            }
1605
1606
                $id = $id . $suffix;
1607
        }
1608
1609
        $newItem->Title = _t(
1610
            'CMSMain.NEWPAGE',
1611
            "New {pagetype}",
1612
            'followed by a page type title',
1613
            array('pagetype' => singleton($className)->i18n_singular_name())
1614
        );
1615
        $newItem->ClassName = $className;
1616
        $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...
1617
1618
        // DataObject::fieldExists only checks the current class, not the hierarchy
1619
        // This allows the CMS to set the correct sort value
1620
        if ($newItem->castingHelper('Sort')) {
1621
            $newItem->Sort = DB::prepared_query('SELECT MAX("Sort") FROM "SiteTree" WHERE "ParentID" = ?', array($parentID))->value() + 1;
1622
        }
1623
1624
        if ($setID) {
1625
            $newItem->ID = $id;
1626
        }
1627
1628
        # Some modules like subsites add extra fields that need to be set when the new item is created
1629
        $this->extend('augmentNewSiteTreeItem', $newItem);
1630
1631
        return $newItem;
1632
    }
1633
1634
    /**
1635
     * Actually perform the publication step
1636
     *
1637
     * @param Versioned|DataObject $record
1638
     * @return mixed
1639
     */
1640
    public function performPublish($record)
1641
    {
1642
        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...
1643
            return Security::permissionFailure($this);
1644
        }
1645
1646
        $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...
1647
    }
1648
1649
    /**
1650
     * Reverts a page by publishing it to live.
1651
     * Use {@link restorepage()} if you want to restore a page
1652
     * which was deleted from draft without publishing.
1653
     *
1654
     * @uses SiteTree->doRevertToLive()
1655
     *
1656
     * @param array $data
1657
     * @param Form $form
1658
     * @return HTTPResponse
1659
     * @throws HTTPResponse_Exception
1660
     */
1661
    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...
1662
    {
1663
        if (!isset($data['ID'])) {
1664
            throw new HTTPResponse_Exception("Please pass an ID in the form content", 400);
1665
        }
1666
1667
        $id = (int) $data['ID'];
1668
        $restoredPage = Versioned::get_latest_version(SiteTree::class, $id);
1669
        if (!$restoredPage) {
1670
            throw new HTTPResponse_Exception("SiteTree #$id not found", 400);
1671
        }
1672
1673
        /** @var SiteTree $record */
1674
        $record = Versioned::get_one_by_stage('SilverStripe\\CMS\\Model\\SiteTree', 'Live', array(
1675
            '"SiteTree_Live"."ID"' => $id
1676
        ));
1677
1678
        // a user can restore a page without publication rights, as it just adds a new draft state
1679
        // (this action should just be available when page has been "deleted from draft")
1680
        if ($record && !$record->canEdit()) {
1681
            return Security::permissionFailure($this);
1682
        }
1683
        if (!$record || !$record->ID) {
1684
            throw new HTTPResponse_Exception("Bad record ID #$id", 404);
1685
        }
1686
1687
        $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...
1688
1689
        $this->getResponse()->addHeader(
1690
            'X-Status',
1691
            rawurlencode(_t(
1692
                'CMSMain.RESTORED',
1693
                "Restored '{title}' successfully",
1694
                'Param %s is a title',
1695
                array('title' => $record->Title)
1696
            ))
1697
        );
1698
1699
        return $this->getResponseNegotiator()->respond($this->getRequest());
1700
    }
1701
1702
    /**
1703
     * Delete the current page from draft stage.
1704
     *
1705
     * @see deletefromlive()
1706
     *
1707
     * @param array $data
1708
     * @param Form $form
1709
     * @return HTTPResponse
1710
     * @throws HTTPResponse_Exception
1711
     */
1712 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...
1713
    {
1714
        $id = $data['ID'];
1715
        $record = SiteTree::get()->byID($id);
1716
        if ($record && !$record->canDelete()) {
1717
            return Security::permissionFailure();
1718
        }
1719
        if (!$record || !$record->ID) {
1720
            throw new HTTPResponse_Exception("Bad record ID #$id", 404);
1721
        }
1722
1723
        // Delete record
1724
        $record->delete();
1725
1726
        $this->getResponse()->addHeader(
1727
            'X-Status',
1728
            rawurlencode(sprintf(_t('CMSMain.REMOVEDPAGEFROMDRAFT', "Removed '%s' from the draft site"), $record->Title))
1729
        );
1730
1731
        // Even if the record has been deleted from stage and live, it can be viewed in "archive mode"
1732
        return $this->getResponseNegotiator()->respond($this->getRequest());
1733
    }
1734
1735
    /**
1736
     * Delete this page from both live and stage
1737
     *
1738
     * @param array $data
1739
     * @param Form $form
1740
     * @return HTTPResponse
1741
     * @throws HTTPResponse_Exception
1742
     */
1743 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...
1744
    {
1745
        $id = $data['ID'];
1746
        /** @var SiteTree $record */
1747
        $record = SiteTree::get()->byID($id);
1748
        if (!$record || !$record->exists()) {
1749
            throw new HTTPResponse_Exception("Bad record ID #$id", 404);
1750
        }
1751
        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...
1752
            return Security::permissionFailure();
1753
        }
1754
1755
        // Archive record
1756
        $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...
1757
1758
        $this->getResponse()->addHeader(
1759
            'X-Status',
1760
            rawurlencode(sprintf(_t('CMSMain.ARCHIVEDPAGE', "Archived page '%s'"), $record->Title))
1761
        );
1762
1763
        // Even if the record has been deleted from stage and live, it can be viewed in "archive mode"
1764
        return $this->getResponseNegotiator()->respond($this->getRequest());
1765
    }
1766
1767
    public function publish($data, $form)
1768
    {
1769
        $data['publish'] = '1';
1770
1771
        return $this->save($data, $form);
1772
    }
1773
1774
    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...
1775
    {
1776
        $className = $this->stat('tree_class');
1777
        /** @var SiteTree $record */
1778
        $record = DataObject::get_by_id($className, $data['ID']);
1779
1780
        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...
1781
            return Security::permissionFailure($this);
1782
        }
1783
        if (!$record || !$record->ID) {
1784
            throw new HTTPResponse_Exception("Bad record ID #" . (int)$data['ID'], 404);
1785
        }
1786
1787
        $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...
1788
1789
        $this->getResponse()->addHeader(
1790
            'X-Status',
1791
            rawurlencode(_t('CMSMain.REMOVEDPAGE', "Removed '{title}' from the published site", array('title' => $record->Title)))
1792
        );
1793
1794
        return $this->getResponseNegotiator()->respond($this->getRequest());
1795
    }
1796
1797
    /**
1798
     * @return HTTPResponse
1799
     */
1800
    public function rollback()
1801
    {
1802
        return $this->doRollback(array(
1803
            'ID' => $this->currentPageID(),
1804
            'Version' => $this->getRequest()->param('VersionID')
1805
        ), 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...
1806
    }
1807
1808
    /**
1809
     * Rolls a site back to a given version ID
1810
     *
1811
     * @param array $data
1812
     * @param Form $form
1813
     * @return HTTPResponse
1814
     */
1815
    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...
1816
    {
1817
        $this->extend('onBeforeRollback', $data['ID']);
1818
1819
        $id = (isset($data['ID'])) ? (int) $data['ID'] : null;
1820
        $version = (isset($data['Version'])) ? (int) $data['Version'] : null;
1821
1822
        /** @var DataObject|Versioned $record */
1823
        $record = DataObject::get_by_id($this->stat('tree_class'), $id);
1824
        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...
1825
            return Security::permissionFailure($this);
1826
        }
1827
1828
        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...
1829
            $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...
1830
            $message = _t(
1831
                'CMSMain.ROLLEDBACKVERSIONv2',
1832
                "Rolled back to version #%d.",
1833
                array('version' => $data['Version'])
1834
            );
1835
        } else {
1836
            $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...
1837
            $message = _t(
1838
                'CMSMain.ROLLEDBACKPUBv2',
1839
                "Rolled back to published version."
1840
            );
1841
        }
1842
1843
        $this->getResponse()->addHeader('X-Status', rawurlencode($message));
1844
1845
        // Can be used in different contexts: In normal page edit view, in which case the redirect won't have any effect.
1846
        // Or in history view, in which case a revert causes the CMS to re-load the edit view.
1847
        // The X-Pjax header forces a "full" content refresh on redirect.
1848
        $url = Controller::join_links(CMSPageEditController::singleton()->Link('show'), $record->ID);
1849
        $this->getResponse()->addHeader('X-ControllerURL', $url);
1850
        $this->getRequest()->addHeader('X-Pjax', 'Content');
1851
        $this->getResponse()->addHeader('X-Pjax', 'Content');
1852
1853
        return $this->getResponseNegotiator()->respond($this->getRequest());
1854
    }
1855
1856
    /**
1857
     * Batch Actions Handler
1858
     */
1859
    public function batchactions()
1860
    {
1861
        return new CMSBatchActionHandler($this, 'batchactions');
1862
    }
1863
1864
    public function BatchActionParameters()
1865
    {
1866
        $batchActions = CMSBatchActionHandler::config()->batch_actions;
1867
1868
        $forms = array();
1869
        foreach ($batchActions as $urlSegment => $batchAction) {
1870
            $SNG_action = singleton($batchAction);
1871
            if ($SNG_action->canView() && $fieldset = $SNG_action->getParameterFields()) {
1872
                $formHtml = '';
1873
                /** @var FormField $field */
1874
                foreach ($fieldset as $field) {
1875
                    $formHtml .= $field->Field();
1876
                }
1877
                $forms[$urlSegment] = $formHtml;
1878
            }
1879
        }
1880
        $pageHtml = '';
1881
        foreach ($forms as $urlSegment => $html) {
1882
            $pageHtml .= "<div class=\"params\" id=\"BatchActionParameters_$urlSegment\">$html</div>\n\n";
1883
        }
1884
        return new LiteralField("BatchActionParameters", '<div id="BatchActionParameters" style="display:none">'.$pageHtml.'</div>');
1885
    }
1886
    /**
1887
     * Returns a list of batch actions
1888
     */
1889
    public function BatchActionList()
1890
    {
1891
        return $this->batchactions()->batchActionList();
1892
    }
1893
1894
    public function publishall($request)
1895
    {
1896
        if (!Permission::check('ADMIN')) {
1897
            return Security::permissionFailure($this);
1898
        }
1899
1900
        increase_time_limit_to();
1901
        increase_memory_limit_to();
1902
1903
        $response = "";
1904
1905
        if (isset($this->requestParams['confirm'])) {
1906
            // Protect against CSRF on destructive action
1907
            if (!SecurityToken::inst()->checkRequest($request)) {
1908
                return $this->httpError(400);
1909
            }
1910
1911
            $start = 0;
1912
            $pages = SiteTree::get()->limit("$start,30");
1913
            $count = 0;
1914
            while ($pages) {
1915
                /** @var SiteTree $page */
1916
                foreach ($pages as $page) {
1917
                    if ($page && !$page->canPublish()) {
1918
                        return Security::permissionFailure($this);
1919
                    }
1920
1921
                    $page->publishRecursive();
1922
                    $page->destroy();
1923
                    unset($page);
1924
                    $count++;
1925
                    $response .= "<li>$count</li>";
1926
                }
1927
                if ($pages->count() > 29) {
1928
                    $start += 30;
1929
                    $pages = SiteTree::get()->limit("$start,30");
1930
                } else {
1931
                    break;
1932
                }
1933
            }
1934
            $response .= _t('CMSMain.PUBPAGES', "Done: Published {count} pages", array('count' => $count));
1935
        } else {
1936
            $token = SecurityToken::inst();
1937
            $fields = new FieldList();
1938
            $token->updateFieldSet($fields);
1939
            $tokenField = $fields->first();
1940
            $tokenHtml = ($tokenField) ? $tokenField->FieldHolder() : '';
1941
            $publishAllDescription = _t(
1942
                'CMSMain.PUBALLFUN2',
1943
                'Pressing this button will do the equivalent of going to every page and pressing "publish".  '
1944
                . 'It\'s intended to be used after there have been massive edits of the content, such as when '
1945
                . 'the site was first built.'
1946
            );
1947
            $response .= '<h1>' . _t('CMSMain.PUBALLFUN', '"Publish All" functionality') . '</h1>
1948
				<p>' . $publishAllDescription . '</p>
1949
				<form method="post" action="publishall">
1950
					<input type="submit" name="confirm" value="'
1951
                    . _t('CMSMain.PUBALLCONFIRM', "Please publish every page in the site, copying content stage to live", 'Confirmation button') .'" />'
1952
                    . $tokenHtml .
1953
                '</form>';
1954
        }
1955
1956
        return $response;
1957
    }
1958
1959
    /**
1960
     * Restore a completely deleted page from the SiteTree_versions table.
1961
     *
1962
     * @param array $data
1963
     * @param Form $form
1964
     * @return HTTPResponse
1965
     */
1966
    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...
1967
    {
1968
        if (!isset($data['ID']) || !is_numeric($data['ID'])) {
1969
            return new HTTPResponse("Please pass an ID in the form content", 400);
1970
        }
1971
1972
        $id = (int)$data['ID'];
1973
        /** @var SiteTree $restoredPage */
1974
        $restoredPage = Versioned::get_latest_version(SiteTree::class, $id);
1975
        if (!$restoredPage) {
1976
            return new HTTPResponse("SiteTree #$id not found", 400);
1977
        }
1978
1979
        $restoredPage = $restoredPage->doRestoreToStage();
1980
1981
        $this->getResponse()->addHeader(
1982
            'X-Status',
1983
            rawurlencode(_t(
1984
                'CMSMain.RESTORED',
1985
                "Restored '{title}' successfully",
1986
                array('title' => $restoredPage->Title)
1987
            ))
1988
        );
1989
1990
        return $this->getResponseNegotiator()->respond($this->getRequest());
1991
    }
1992
1993
    public function duplicate($request)
1994
    {
1995
        // Protect against CSRF on destructive action
1996
        if (!SecurityToken::inst()->checkRequest($request)) {
1997
            return $this->httpError(400);
1998
        }
1999
2000
        if (($id = $this->urlParams['ID']) && is_numeric($id)) {
2001
            /** @var SiteTree $page */
2002
            $page = SiteTree::get()->byID($id);
2003 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...
2004
                return Security::permissionFailure($this);
2005
            }
2006
            if (!$page || !$page->ID) {
2007
                throw new HTTPResponse_Exception("Bad record ID #$id", 404);
2008
            }
2009
2010
            $newPage = $page->duplicate();
2011
2012
            // ParentID can be hard-set in the URL.  This is useful for pages with multiple parents
2013
            if (isset($_GET['parentID']) && is_numeric($_GET['parentID'])) {
2014
                $newPage->ParentID = $_GET['parentID'];
2015
                $newPage->write();
2016
            }
2017
2018
            $this->getResponse()->addHeader(
2019
                'X-Status',
2020
                rawurlencode(_t(
2021
                    'CMSMain.DUPLICATED',
2022
                    "Duplicated '{title}' successfully",
2023
                    array('title' => $newPage->Title)
2024
                ))
2025
            );
2026
            $url = Controller::join_links(CMSPageEditController::singleton()->Link('show'), $newPage->ID);
2027
            $this->getResponse()->addHeader('X-ControllerURL', $url);
2028
            $this->getRequest()->addHeader('X-Pjax', 'Content');
2029
            $this->getResponse()->addHeader('X-Pjax', 'Content');
2030
2031
            return $this->getResponseNegotiator()->respond($this->getRequest());
2032
        } else {
2033
            return new HTTPResponse("CMSMain::duplicate() Bad ID: '$id'", 400);
2034
        }
2035
    }
2036
2037
    public function duplicatewithchildren($request)
2038
    {
2039
        // Protect against CSRF on destructive action
2040
        if (!SecurityToken::inst()->checkRequest($request)) {
2041
            return $this->httpError(400);
2042
        }
2043
        increase_time_limit_to();
2044
        if (($id = $this->urlParams['ID']) && is_numeric($id)) {
2045
            /** @var SiteTree $page */
2046
            $page = SiteTree::get()->byID($id);
2047 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...
2048
                return Security::permissionFailure($this);
2049
            }
2050
            if (!$page || !$page->ID) {
2051
                throw new HTTPResponse_Exception("Bad record ID #$id", 404);
2052
            }
2053
2054
            $newPage = $page->duplicateWithChildren();
2055
2056
            $this->getResponse()->addHeader(
2057
                'X-Status',
2058
                rawurlencode(_t(
2059
                    'CMSMain.DUPLICATEDWITHCHILDREN',
2060
                    "Duplicated '{title}' and children successfully",
2061
                    array('title' => $newPage->Title)
2062
                ))
2063
            );
2064
            $url = Controller::join_links(CMSPageEditController::singleton()->Link('show'), $newPage->ID);
2065
            $this->getResponse()->addHeader('X-ControllerURL', $url);
2066
            $this->getRequest()->addHeader('X-Pjax', 'Content');
2067
            $this->getResponse()->addHeader('X-Pjax', 'Content');
2068
2069
            return $this->getResponseNegotiator()->respond($this->getRequest());
2070
        } else {
2071
            return new HTTPResponse("CMSMain::duplicatewithchildren() Bad ID: '$id'", 400);
2072
        }
2073
    }
2074
2075
    public function providePermissions()
2076
    {
2077
        $title = CMSPagesController::menu_title();
2078
        return array(
2079
            "CMS_ACCESS_CMSMain" => array(
2080
                'name' => _t('CMSMain.ACCESS', "Access to '{title}' section", array('title' => $title)),
2081
                'category' => _t('Permission.CMS_ACCESS_CATEGORY', 'CMS Access'),
2082
                'help' => _t(
2083
                    'CMSMain.ACCESS_HELP',
2084
                    '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".'
2085
                ),
2086
                'sort' => -99 // below "CMS_ACCESS_LeftAndMain", but above everything else
2087
            )
2088
        );
2089
    }
2090
}
2091