Completed
Pull Request — master (#1811)
by Damian
02:24
created

CMSMain::getSiteTreeFor()   D

Complexity

Conditions 10
Paths 288

Size

Total Lines 58
Code Lines 33

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 58
rs 4.9315
c 0
b 0
f 0
cc 10
eloc 33
nc 288
nop 6

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
namespace SilverStripe\CMS\Controllers;
4
5
use SilverStripe\Admin\AdminRootController;
6
use SilverStripe\Admin\CMSBatchActionHandler;
7
use SilverStripe\Admin\LeftAndMain_SearchFilter;
8
use SilverStripe\Admin\LeftAndMainFormRequestHandler;
9
use SilverStripe\CMS\Model\VirtualPage;
10
use SilverStripe\Forms\Tab;
11
use SilverStripe\ORM\CMSPreviewable;
12
use SilverStripe\Admin\LeftAndMain;
13
use SilverStripe\CMS\BatchActions\CMSBatchAction_Archive;
14
use SilverStripe\CMS\BatchActions\CMSBatchAction_Publish;
15
use SilverStripe\CMS\BatchActions\CMSBatchAction_Restore;
16
use SilverStripe\CMS\BatchActions\CMSBatchAction_Unpublish;
17
use SilverStripe\CMS\Model\CurrentPageIdentifier;
18
use SilverStripe\CMS\Model\RedirectorPage;
19
use SilverStripe\CMS\Model\SiteTree;
20
use SilverStripe\Control\Controller;
21
use SilverStripe\Control\Director;
22
use SilverStripe\Control\Session;
23
use SilverStripe\Control\HTTPRequest;
24
use SilverStripe\Control\HTTPResponse;
25
use SilverStripe\Control\HTTPResponse_Exception;
26
use SilverStripe\Core\Convert;
27
use SilverStripe\Core\Injector\Injector;
28
use Psr\SimpleCache\CacheInterface;
29
use SilverStripe\Forms\DateField;
30
use SilverStripe\Forms\DropdownField;
31
use SilverStripe\Forms\FieldGroup;
32
use SilverStripe\Forms\FieldList;
33
use SilverStripe\Forms\Form;
34
use SilverStripe\Forms\FormAction;
35
use SilverStripe\Forms\FormField;
36
use SilverStripe\Forms\GridField\GridField;
37
use SilverStripe\Forms\GridField\GridFieldConfig;
38
use SilverStripe\Forms\GridField\GridFieldDataColumns;
39
use SilverStripe\Forms\GridField\GridFieldLevelup;
40
use SilverStripe\Forms\GridField\GridFieldPaginator;
41
use SilverStripe\Forms\GridField\GridFieldSortableHeader;
42
use SilverStripe\Forms\HiddenField;
43
use SilverStripe\Forms\LabelField;
44
use SilverStripe\Forms\LiteralField;
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\Hierarchy\MarkedSet;
54
use SilverStripe\ORM\SS_List;
55
use SilverStripe\ORM\ValidationResult;
56
use SilverStripe\Security\InheritedPermissions;
57
use SilverStripe\SiteConfig\SiteConfig;
58
use SilverStripe\Versioned\Versioned;
59
use SilverStripe\Security\Member;
60
use SilverStripe\Security\Permission;
61
use SilverStripe\Security\PermissionProvider;
62
use SilverStripe\Security\Security;
63
use SilverStripe\Security\SecurityToken;
64
use SilverStripe\View\ArrayData;
65
use SilverStripe\View\Requirements;
66
use Translatable;
67
use InvalidArgumentException;
68
use SilverStripe\Versioned\ChangeSet;
69
use SilverStripe\Versioned\ChangeSetItem;
70
71
/**
72
 * The main "content" area of the CMS.
73
 *
74
 * This class creates a 2-frame layout - left-tree and right-form - to sit beneath the main
75
 * admin menu.
76
 *
77
 * @todo Create some base classes to contain the generic functionality that will be replicated.
78
 *
79
 * @mixin LeftAndMainPageIconsExtension
80
 */
81
class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionProvider
82
{
83
84
    private static $url_segment = 'pages';
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
85
86
    private static $url_rule = '/$Action/$ID/$OtherID';
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
87
88
    // Maintain a lower priority than other administration sections
89
    // so that Director does not think they are actions of CMSMain
90
    private static $url_priority = 39;
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
91
92
    private static $menu_title = 'Edit Page';
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
93
94
    private static $menu_icon_class = 'font-icon-sitemap';
95
96
    private static $menu_priority = 10;
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
97
98
    private static $tree_class = SiteTree::class;
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
99
100
    private static $subitem_class = Member::class;
101
102
    private static $session_namespace = self::class;
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
103
104
    private static $required_permission_codes = 'CMS_ACCESS_CMSMain';
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
105
106
    /**
107
     * Amount of results showing on a single page.
108
     *
109
     * @config
110
     * @var int
111
     */
112
    private static $page_length = 15;
113
114
    private static $allowed_actions = array(
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
115
        'archive',
116
        'deleteitems',
117
        'DeleteItemsForm',
118
        'dialog',
119
        'duplicate',
120
        'duplicatewithchildren',
121
        'publishall',
122
        'publishitems',
123
        'PublishItemsForm',
124
        'submit',
125
        'EditForm',
126
        'SearchForm',
127
        'SiteTreeAsUL',
128
        'getshowdeletedsubtree',
129
        'savetreenode',
130
        'getsubtree',
131
        'updatetreenodes',
132
        'batchactions',
133
        'treeview',
134
        'listview',
135
        'ListViewForm',
136
        'childfilter',
137
    );
138
139
    private static $url_handlers = [
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
140
        'EditForm/$ID' => 'EditForm',
141
    ];
142
143
    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...
144
        'TreeIsFiltered' => 'Boolean',
145
        'AddForm' => 'HTMLFragment',
146
        'LinkPages' => 'Text',
147
        'Link' => 'Text',
148
        'ListViewForm' => 'HTMLFragment',
149
        'ExtraTreeTools' => 'HTMLFragment',
150
        'PageList' => 'HTMLFragment',
151
        'PageListSidebar' => 'HTMLFragment',
152
        'SiteTreeHints' => 'HTMLFragment',
153
        'SecurityID' => 'Text',
154
        'SiteTreeAsUL' => 'HTMLFragment',
155
    );
156
157
    protected function init()
158
    {
159
        // set reading lang
160
        if (SiteTree::has_extension('Translatable') && !$this->getRequest()->isAjax()) {
161
            Translatable::choose_site_locale(array_keys(Translatable::get_existing_content_languages('SilverStripe\\CMS\\Model\\SiteTree')));
162
        }
163
164
        parent::init();
165
166
        Requirements::javascript(CMS_DIR . '/client/dist/js/bundle.js');
167
        Requirements::javascript(CMS_DIR . '/client/dist/js/SilverStripeNavigator.js');
168
        Requirements::css(CMS_DIR . '/client/dist/styles/bundle.css');
169
        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...
170
        Requirements::add_i18n_javascript(CMS_DIR . '/client/lang', false, true);
171
172
        CMSBatchActionHandler::register('restore', CMSBatchAction_Restore::class);
173
        CMSBatchActionHandler::register('archive', CMSBatchAction_Archive::class);
174
        CMSBatchActionHandler::register('unpublish', CMSBatchAction_Unpublish::class);
175
        CMSBatchActionHandler::register('publish', CMSBatchAction_Publish::class);
176
    }
177
178
    public function index($request)
179
    {
180
        // In case we're not showing a specific record, explicitly remove any session state,
181
        // to avoid it being highlighted in the tree, and causing an edit form to show.
182
        if (!$request->param('Action')) {
183
            $this->setCurrentPageID(null);
184
        }
185
186
        return parent::index($request);
187
    }
188
189
    public function getResponseNegotiator()
190
    {
191
        $negotiator = parent::getResponseNegotiator();
192
193
        // ListViewForm
194
        $negotiator->setCallback('ListViewForm', function () {
195
            return $this->ListViewForm()->forTemplate();
196
        });
197
198
        // PageList view
199
        $negotiator->setCallback('Content-PageList', function () {
200
            return $this->PageList()->forTemplate();
201
        });
202
203
        // PageList view for edit controller
204
        $negotiator->setCallback('Content-PageList-Sidebar', function () {
205
            return $this->PageListSidebar()->forTemplate();
206
        });
207
208
        return $negotiator;
209
    }
210
211
    /**
212
     * Get pages listing area
213
     *
214
     * @return DBHTMLText
215
     */
216
    public function PageList()
217
    {
218
        return $this->renderWith($this->getTemplatesWithSuffix('_PageList'));
219
    }
220
221
    /**
222
     * Page list view for edit-form
223
     *
224
     * @return DBHTMLText
225
     */
226
    public function PageListSidebar()
227
    {
228
        return $this->renderWith($this->getTemplatesWithSuffix('_PageList_Sidebar'));
229
    }
230
231
    /**
232
     * If this is set to true, the "switchView" context in the
233
     * template is shown, with links to the staging and publish site.
234
     *
235
     * @return boolean
236
     */
237
    public function ShowSwitchView()
238
    {
239
        return true;
240
    }
241
242
    /**
243
     * Overloads the LeftAndMain::ShowView. Allows to pass a page as a parameter, so we are able
244
     * to switch view also for archived versions.
245
     *
246
     * @param SiteTree $page
247
     * @return array
248
     */
249
    public function SwitchView($page = null)
250
    {
251
        if (!$page) {
252
            $page = $this->currentPage();
253
        }
254
255
        if ($page) {
256
            $nav = SilverStripeNavigator::get_for_record($page);
257
            return $nav['items'];
258
        }
259
    }
260
261
    //------------------------------------------------------------------------------------------//
262
    // Main controllers
263
264
    //------------------------------------------------------------------------------------------//
265
    // Main UI components
266
267
    /**
268
     * Override {@link LeftAndMain} Link to allow blank URL segment for CMSMain.
269
     *
270
     * @param string|null $action Action to link to.
271
     * @return string
272
     */
273
    public function Link($action = null)
274
    {
275
        $link = Controller::join_links(
276
            AdminRootController::admin_url(),
277
            $this->stat('url_segment'), // in case we want to change the segment
278
            '/', // trailing slash needed if $action is null!
279
            "$action"
280
        );
281
        $this->extend('updateLink', $link);
282
        return $link;
283
    }
284
285
    public function LinkPages()
286
    {
287
        return CMSPagesController::singleton()->Link();
288
    }
289
290
    public function LinkPagesWithSearch()
291
    {
292
        return $this->LinkWithSearch($this->LinkPages());
293
    }
294
295
    /**
296
     * Get link to tree view
297
     *
298
     * @return string
299
     */
300
    public function LinkTreeView()
301
    {
302
        // Tree view is just default link to main pages section (no /treeview suffix)
303
        return $this->LinkWithSearch(CMSMain::singleton()->Link());
304
    }
305
306
    /**
307
     * Get link to list view
308
     *
309
     * @return string
310
     */
311
    public function LinkListView()
312
    {
313
        // Note : Force redirect to top level page controller
314
        return $this->LinkWithSearch(CMSMain::singleton()->Link('listview'));
315
    }
316
317 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...
318
    {
319
        if (!$id) {
320
            $id = $this->currentPageID();
321
        }
322
        return $this->LinkWithSearch(
323
            Controller::join_links(CMSPageEditController::singleton()->Link('show'), $id)
324
        );
325
    }
326
327 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...
328
    {
329
        if ($id = $this->currentPageID()) {
330
            return $this->LinkWithSearch(
331
                Controller::join_links(CMSPageSettingsController::singleton()->Link('show'), $id)
332
            );
333
        } else {
334
            return null;
335
        }
336
    }
337
338 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...
339
    {
340
        if ($id = $this->currentPageID()) {
341
            return $this->LinkWithSearch(
342
                Controller::join_links(CMSPageHistoryController::singleton()->Link('show'), $id)
343
            );
344
        } else {
345
            return null;
346
        }
347
    }
348
349
    public function LinkWithSearch($link)
350
    {
351
        // Whitelist to avoid side effects
352
        $params = array(
353
            'q' => (array)$this->getRequest()->getVar('q'),
354
            'ParentID' => $this->getRequest()->getVar('ParentID')
355
        );
356
        $link = Controller::join_links(
357
            $link,
358
            array_filter(array_values($params)) ? '?' . http_build_query($params) : null
359
        );
360
        $this->extend('updateLinkWithSearch', $link);
361
        return $link;
362
    }
363
364
    public function LinkPageAdd($extra = null, $placeholders = null)
365
    {
366
        $link = CMSPageAddController::singleton()->Link();
367
        $this->extend('updateLinkPageAdd', $link);
368
369
        if ($extra) {
370
            $link = Controller::join_links($link, $extra);
371
        }
372
373
        if ($placeholders) {
374
            $link .= (strpos($link, '?') === false ? "?$placeholders" : "&$placeholders");
375
        }
376
377
        return $link;
378
    }
379
380
    /**
381
     * @return string
382
     */
383
    public function LinkPreview()
384
    {
385
        $record = $this->getRecord($this->currentPageID());
386
        $baseLink = Director::absoluteBaseURL();
387
        if ($record && $record instanceof SiteTree) {
388
            // if we are an external redirector don't show a link
389
            if ($record instanceof RedirectorPage && $record->RedirectionType == 'External') {
390
                $baseLink = false;
391
            } else {
392
                $baseLink = $record->Link('?stage=Stage');
393
            }
394
        }
395
        return $baseLink;
396
    }
397
398
    /**
399
     * Return the entire site tree as a nested set of ULs
400
     */
401
    public function SiteTreeAsUL()
402
    {
403
        // Pre-cache sitetree version numbers for querying efficiency
404
        Versioned::prepopulate_versionnumber_cache(SiteTree::class, Versioned::DRAFT);
405
        Versioned::prepopulate_versionnumber_cache(SiteTree::class, Versioned::LIVE);
406
        $html = $this->getSiteTreeFor($this->stat('tree_class'));
407
408
        $this->extend('updateSiteTreeAsUL', $html);
409
410
        return $html;
411
    }
412
413
    /**
414
     * Get a site tree HTML listing which displays the nodes under the given criteria.
415
     *
416
     * @param string $className The class of the root object
417
     * @param string $rootID The ID of the root object.  If this is null then a complete tree will be
418
     *  shown
419
     * @param string $childrenMethod The method to call to get the children of the tree. For example,
420
     *  Children, AllChildrenIncludingDeleted, or AllHistoricalChildren
421
     * @param string $numChildrenMethod
422
     * @param callable $filterFunction
423
     * @param int $nodeCountThreshold
424
     * @return string Nested unordered list with links to each page
425
     */
426
    public function getSiteTreeFor(
427
        $className,
428
        $rootID = null,
429
        $childrenMethod = null,
430
        $numChildrenMethod = null,
431
        $filterFunction = null,
432
        $nodeCountThreshold = 30
433
    ) {
434
        // Provide better defaults from filter
435
        $filter = $this->getSearchFilter();
436
        if ($filter) {
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->getChildrenMethod();
439
            }
440
            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...
441
                $numChildrenMethod = $filter->getNumChildrenMethod();
442
            }
443
            if (!$filterFunction) {
444
                $filterFunction = function ($node) use ($filter) {
445
                    return $filter->isPageIncluded($node);
446
                };
447
            }
448
        }
449
450
        // Build set from node and begin marking
451
        $record = ($rootID) ? $this->getRecord($rootID) : null;
452
        $rootNode = $record ? $record : DataObject::singleton($className);
453
        $markingSet = MarkedSet::create($rootNode, $childrenMethod, $numChildrenMethod, $nodeCountThreshold);
454
455
        // Set filter function
456
        if ($filterFunction) {
457
            $markingSet->setMarkingFilterFunction($filterFunction);
458
        }
459
460
        // Mark tree from this node
461
        $markingSet->markPartialTree();
462
463
        // Ensure current page is exposed
464
        $currentPage = $this->currentPage();
465
        if ($currentPage) {
466
            $markingSet->markToExpose($currentPage);
467
        }
468
469
        // Pre-cache permissions
470
        $checker = SiteTree::getPermissionChecker();
471
        if ($checker instanceof InheritedPermissions) {
0 ignored issues
show
Bug introduced by
The class SilverStripe\Security\InheritedPermissions does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
472
            $checker->prePopulatePermissionCache(
473
                InheritedPermissions::EDIT,
474
                $markingSet->markedNodeIDs()
475
            );
476
        }
477
478
        // Render using full-subtree template
479
        return $markingSet->renderChildren(
480
            [ self::class . '_SubTree', 'type' => 'Includes' ],
0 ignored issues
show
Documentation introduced by
array(self::class . '_Su..., 'type' => 'Includes') is of type array<integer|string,str...ring","type":"string"}>, but the function expects a string|null.

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
481
            $this->getTreeNodeCustomisations()
482
        );
483
    }
484
485
486
    /**
487
     * Get callback to determine template customisations for nodes
488
     *
489
     * @return callable
490
     */
491
    protected function getTreeNodeCustomisations()
492
    {
493
        $rootTitle = $this->getCMSTreeTitle();
494
        $linkWithSearch = $this->LinkWithSearch($this->Link());
495
        return function (SiteTree $node) use ($linkWithSearch, $rootTitle) {
496
            return [
497
                'listViewLink' => Controller::join_links(
498
                    $linkWithSearch,
499
                    '?view=listview&ParentID=' . $node->ID
500
                ),
501
                'rootTitle' => $rootTitle,
502
                'extraClass' => $this->getTreeNodeClasses($node),
503
            ];
504
        };
505
    }
506
507
    /**
508
     * Get extra CSS classes for a page's tree node
509
     *
510
     * @param SiteTree $node
511
     * @return string
512
     */
513
    public function getTreeNodeClasses(SiteTree $node)
514
    {
515
        // Get classes from object
516
        $classes = $node->CMSTreeClasses();
517
518
        // Flag as current
519
        if ($this->isCurrentPage($node)) {
520
            $classes .= ' current';
521
        }
522
523
        // Get status flag classes
524
        $flags = $node->getStatusFlags();
525
        if ($flags) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $flags of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

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

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
620
                $this->getTreeNodeCustomisations()
621
            );
622
623
            $data[$id] = array(
624
                'html' => $html,
625
                'ParentID' => $record->ParentID,
626
                'NextID' => $next ? $next->ID : null,
627
                'PrevID' => $prev ? $prev->ID : null
628
            );
629
        }
630
        return $this
631
            ->getResponse()
632
            ->addHeader('Content-Type', 'application/json')
633
            ->setBody(Convert::raw2json($data));
634
    }
635
636
    /**
637
     * Update the position and parent of a tree node.
638
     * Only saves the node if changes were made.
639
     *
640
     * Required data:
641
     * - 'ID': The moved node
642
     * - 'ParentID': New parent relation of the moved node (0 for root)
643
     * - 'SiblingIDs': Array of all sibling nodes to the moved node (incl. the node itself).
644
     *   In case of a 'ParentID' change, relates to the new siblings under the new parent.
645
     *
646
     * @param HTTPRequest $request
647
     * @return HTTPResponse JSON string with a
648
     * @throws HTTPResponse_Exception
649
     */
650
    public function savetreenode($request)
651
    {
652
        if (!SecurityToken::inst()->checkRequest($request)) {
653
            return $this->httpError(400);
654
        }
655
        if (!Permission::check('SITETREE_REORGANISE') && !Permission::check('ADMIN')) {
656
            return $this->httpError(
657
                403,
658
                _t(
659
                    __CLASS__.'.CANT_REORGANISE',
660
                    "You do not have permission to rearange the site tree. Your change was not saved."
661
                )
662
            );
663
        }
664
665
        $className = $this->stat('tree_class');
666
        $id = $request->requestVar('ID');
667
        $parentID = $request->requestVar('ParentID');
668
        if (!is_numeric($id) || !is_numeric($parentID)) {
669
            return $this->httpError(400);
670
        }
671
672
        // Check record exists in the DB
673
        /** @var SiteTree $node */
674
        $node = DataObject::get_by_id($className, $id);
675
        if (!$node) {
676
            return $this->httpError(
677
                500,
678
                _t(
679
                    __CLASS__.'.PLEASESAVE',
680
                    "Please Save Page: This page could not be updated because it hasn't been saved yet."
681
                )
682
            );
683
        }
684
685
        // Check top level permissions
686
        $root = $node->getParentType();
687
        if (($parentID == '0' || $root == 'root') && !SiteConfig::current_site_config()->canCreateTopLevel()) {
688
            return $this->httpError(
689
                403,
690
                _t(
691
                    __CLASS__.'.CANT_REORGANISE',
692
                    "You do not have permission to alter Top level pages. Your change was not saved."
693
                )
694
            );
695
        }
696
697
        $siblingIDs = $request->requestVar('SiblingIDs');
698
        $statusUpdates = array('modified'=>array());
699
700
        if (!$node->canEdit()) {
701
            return Security::permissionFailure($this);
702
        }
703
704
        // Update hierarchy (only if ParentID changed)
705
        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...
706
            $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...
707
            $node->write();
708
709
            $statusUpdates['modified'][$node->ID] = array(
710
                '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...
711
            );
712
713
            // Update all dependent pages
714
            $virtualPages = VirtualPage::get()->filter("CopyContentFromID", $node->ID);
715
            foreach ($virtualPages as $virtualPage) {
716
                $statusUpdates['modified'][$virtualPage->ID] = array(
717
                    'TreeTitle' => $virtualPage->TreeTitle()
718
                );
719
            }
720
721
            $this->getResponse()->addHeader(
722
                'X-Status',
723
                rawurlencode(_t(__CLASS__.'.REORGANISATIONSUCCESSFUL', 'Reorganised the site tree successfully.'))
724
            );
725
        }
726
727
        // Update sorting
728
        if (is_array($siblingIDs)) {
729
            $counter = 0;
730
            foreach ($siblingIDs as $id) {
731
                if ($id == $node->ID) {
732
                    $node->Sort = ++$counter;
733
                    $node->write();
734
                    $statusUpdates['modified'][$node->ID] = array(
735
                        '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...
736
                    );
737
                } elseif (is_numeric($id)) {
738
                    // Nodes that weren't "actually moved" shouldn't be registered as
739
                    // having been edited; do a direct SQL update instead
740
                    ++$counter;
741
                    $table = DataObject::getSchema()->baseDataTable($className);
742
                    DB::prepared_query(
743
                        "UPDATE \"$table\" SET \"Sort\" = ? WHERE \"ID\" = ?",
744
                        array($counter, $id)
745
                    );
746
                }
747
            }
748
749
            $this->getResponse()->addHeader(
750
                'X-Status',
751
                rawurlencode(_t(__CLASS__.'.REORGANISATIONSUCCESSFUL', 'Reorganised the site tree successfully.'))
752
            );
753
        }
754
755
        return $this
756
            ->getResponse()
757
            ->addHeader('Content-Type', 'application/json')
758
            ->setBody(Convert::raw2json($statusUpdates));
759
    }
760
761
    public function CanOrganiseSitetree()
762
    {
763
        return !Permission::check('SITETREE_REORGANISE') && !Permission::check('ADMIN') ? false : true;
764
    }
765
766
    /**
767
     * @return boolean
768
     */
769
    public function TreeIsFiltered()
770
    {
771
        $query = $this->getRequest()->getVar('q');
772
773
        if (!$query || (count($query) === 1 && isset($query['FilterClass']) && $query['FilterClass'] === 'SilverStripe\\CMS\\Controllers\\CMSSiteTreeFilter_Search')) {
774
            return false;
775
        }
776
777
        return true;
778
    }
779
780
    public function ExtraTreeTools()
781
    {
782
        $html = '';
783
        $this->extend('updateExtraTreeTools', $html);
784
        return $html;
785
    }
786
787
    /**
788
     * Returns a Form for page searching for use in templates.
789
     *
790
     * Can be modified from a decorator by a 'updateSearchForm' method
791
     *
792
     * @return Form
793
     */
794
    public function SearchForm()
795
    {
796
        // Create the fields
797
        $content = new TextField('q[Term]', _t('SilverStripe\\CMS\\Search\\SearchForm.FILTERLABELTEXT', 'Search'));
798
        $dateFrom = new DateField(
799
            'q[LastEditedFrom]',
800
            _t('SilverStripe\\CMS\\Search\\SearchForm.FILTERDATEFROM', 'From')
801
        );
802
        $dateTo = new DateField(
803
            'q[LastEditedTo]',
804
            _t('SilverStripe\\CMS\\Search\\SearchForm.FILTERDATETO', 'To')
805
        );
806
        $pageFilter = new DropdownField(
807
            'q[FilterClass]',
808
            _t('SilverStripe\\CMS\\Controllers\\CMSMain.PAGES', 'Page status'),
809
            CMSSiteTreeFilter::get_all_filters()
810
        );
811
        $pageClasses = new DropdownField(
812
            'q[ClassName]',
813
            _t('SilverStripe\\CMS\\Controllers\\CMSMain.PAGETYPEOPT', 'Page type', 'Dropdown for limiting search to a page type'),
814
            $this->getPageTypes()
815
        );
816
        $pageClasses->setEmptyString(_t('SilverStripe\\CMS\\Controllers\\CMSMain.PAGETYPEANYOPT', 'Any'));
817
818
        // Group the Datefields
819
        $dateGroup = new FieldGroup(
820
            $dateFrom,
821
            $dateTo
822
        );
823
        $dateGroup->setTitle(_t('SilverStripe\\CMS\\Search\\SearchForm.PAGEFILTERDATEHEADING', 'Last edited'));
824
825
        // view mode
826
        $viewMode = HiddenField::create('view', false, $this->ViewState());
827
828
        // Create the Field list
829
        $fields = new FieldList(
830
            $content,
831
            $pageFilter,
832
            $pageClasses,
833
            $dateGroup,
834
            $viewMode
835
        );
836
837
        // Create the Search and Reset action
838
        $actions = new FieldList(
839
            FormAction::create('doSearch', _t('SilverStripe\\CMS\\Controllers\\CMSMain.APPLY_FILTER', 'Search'))
840
                ->addExtraClass('btn btn-primary'),
841
            FormAction::create('clear', _t('SilverStripe\\CMS\\Controllers\\CMSMain.CLEAR_FILTER', 'Clear'))
842
                ->setAttribute('type', 'reset')
843
                ->addExtraClass('btn btn-secondary')
844
        );
845
846
        // Use <button> to allow full jQuery UI styling on the all of the Actions
847
        /** @var FormAction $action */
848
        foreach ($actions->dataFields() as $action) {
849
            /** @var FormAction $action */
850
            $action->setUseButtonTag(true);
851
        }
852
853
        // Create the form
854
        /** @skipUpgrade */
855
        $form = Form::create($this, 'SearchForm', $fields, $actions)
856
            ->addExtraClass('cms-search-form')
857
            ->setFormMethod('GET')
858
            ->setFormAction($this->Link())
859
            ->disableSecurityToken()
860
            ->unsetValidator();
861
862
        // Load the form with previously sent search data
863
        $form->loadDataFrom($this->getRequest()->getVars());
864
865
        // Allow decorators to modify the form
866
        $this->extend('updateSearchForm', $form);
867
868
        return $form;
869
    }
870
871
    /**
872
     * Returns a sorted array suitable for a dropdown with pagetypes and their translated name
873
     *
874
     * @return array
875
     */
876
    protected function getPageTypes()
877
    {
878
        $pageTypes = array();
879
        foreach (SiteTree::page_type_classes() as $pageTypeClass) {
880
            $pageTypes[$pageTypeClass] = SiteTree::singleton($pageTypeClass)->i18n_singular_name();
881
        }
882
        asort($pageTypes);
883
        return $pageTypes;
884
    }
885
886
    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...
887
    {
888
        return $this->getsubtree($this->getRequest());
889
    }
890
891
    /**
892
     * @param bool $unlinked
893
     * @return ArrayList
894
     */
895
    public function Breadcrumbs($unlinked = false)
896
    {
897
        $items = parent::Breadcrumbs($unlinked);
898
899
        if ($items->count() > 1) {
900
            // Specific to the SiteTree admin section, we never show the cms section and current
901
            // page in the same breadcrumbs block.
902
            $items->shift();
903
        }
904
905
        return $items;
906
    }
907
908
    /**
909
     * Create serialized JSON string with site tree hints data to be injected into
910
     * 'data-hints' attribute of root node of jsTree.
911
     *
912
     * @return string Serialized JSON
913
     */
914
    public function SiteTreeHints()
915
    {
916
        $classes = SiteTree::page_type_classes();
917
918
        $cacheCanCreate = array();
919
        foreach ($classes as $class) {
920
            $cacheCanCreate[$class] = singleton($class)->canCreate();
921
        }
922
923
        // Generate basic cache key. Too complex to encompass all variations
924
        $cache = Injector::inst()->get(CacheInterface::class . '.CMSMain_SiteTreeHints');
925
        $cacheKey = md5(implode('_', array(Member::currentUserID(), implode(',', $cacheCanCreate), implode(',', $classes))));
926
        if ($this->getRequest()->getVar('flush')) {
927
            $cache->clear();
928
        }
929
        $json = $cache->get($cacheKey);
930
        if (!$json) {
931
            $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...
932
            $def['Root']['disallowedChildren'] = array();
933
934
            // Contains all possible classes to support UI controls listing them all,
935
            // such as the "add page here" context menu.
936
            $def['All'] = array();
937
938
            // Identify disallows and set globals
939
            foreach ($classes as $class) {
940
                $obj = singleton($class);
941
                if ($obj instanceof HiddenClass) {
942
                    continue;
943
                }
944
945
                // Name item
946
                $def['All'][$class] = array(
947
                    'title' => $obj->i18n_singular_name()
948
                );
949
950
                // Check if can be created at the root
951
                $needsPerm = $obj->stat('need_permission');
952
                if (!$obj->stat('can_be_root')
953
                    || (!array_key_exists($class, $cacheCanCreate) || !$cacheCanCreate[$class])
954
                    || ($needsPerm && !$this->can($needsPerm))
955
                ) {
956
                    $def['Root']['disallowedChildren'][] = $class;
957
                }
958
959
                // Hint data specific to the class
960
                $def[$class] = array();
961
962
                $defaultChild = $obj->defaultChild();
963
                if ($defaultChild !== 'Page' && $defaultChild !== null) {
964
                    $def[$class]['defaultChild'] = $defaultChild;
965
                }
966
967
                $defaultParent = $obj->defaultParent();
968
                if ($defaultParent !== 1 && $defaultParent !== null) {
969
                    $def[$class]['defaultParent'] = $defaultParent;
970
                }
971
            }
972
973
            $this->extend('updateSiteTreeHints', $def);
974
975
            $json = Convert::raw2json($def);
976
            $cache->set($cacheKey, $json);
977
        }
978
        return $json;
979
    }
980
981
    /**
982
     * Populates an array of classes in the CMS
983
     * which allows the user to change the page type.
984
     *
985
     * @return SS_List
986
     */
987
    public function PageTypes()
988
    {
989
        $classes = SiteTree::page_type_classes();
990
991
        $result = new ArrayList();
992
993
        foreach ($classes as $class) {
994
            $instance = SiteTree::singleton($class);
995
            if ($instance instanceof HiddenClass) {
996
                continue;
997
            }
998
999
            // skip this type if it is restricted
1000
            if ($instance->stat('need_permission') && !$this->can(singleton($class)->stat('need_permission'))) {
1001
                continue;
1002
            }
1003
1004
            $singularName = $instance->i18n_singular_name();
1005
            $description = $instance->i18n_classDescription();
1006
1007
            $result->push(new ArrayData(array(
1008
                'ClassName' => $class,
1009
                'AddAction' => $singularName,
1010
                'Description' => $description,
1011
                // TODO Sprite support
1012
                'IconURL' => $instance->stat('icon'),
1013
                'Title' => $singularName,
1014
            )));
1015
        }
1016
1017
        $result = $result->sort('AddAction');
1018
1019
        return $result;
1020
    }
1021
1022
    /**
1023
     * Get a database record to be managed by the CMS.
1024
     *
1025
     * @param int $id Record ID
1026
     * @param int $versionID optional Version id of the given record
1027
     * @return SiteTree
1028
     */
1029
    public function getRecord($id, $versionID = null)
1030
    {
1031
        if (!$id) {
1032
            return null;
1033
        }
1034
        $treeClass = $this->stat('tree_class');
1035
        if ($id instanceof $treeClass) {
1036
            return $id;
1037
        }
1038
        if (substr($id, 0, 3) == 'new') {
1039
            return $this->getNewItem($id);
1040
        }
1041
        if (!is_numeric($id)) {
1042
            return null;
1043
        }
1044
1045
        $currentStage = Versioned::get_reading_mode();
1046
1047
        if ($this->getRequest()->getVar('Version')) {
1048
            $versionID = (int) $this->getRequest()->getVar('Version');
1049
        }
1050
1051
        /** @var SiteTree $record */
1052
        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...
1053
            $record = Versioned::get_version($treeClass, $id, $versionID);
1054
        } else {
1055
            $record = DataObject::get_by_id($treeClass, $id);
1056
        }
1057
1058
        // Then, try getting a record from the live site
1059
        if (!$record) {
1060
            // $record = Versioned::get_one_by_stage($treeClass, "Live", "\"$treeClass\".\"ID\" = $id");
1061
            Versioned::set_stage(Versioned::LIVE);
1062
            singleton($treeClass)->flushCache();
1063
1064
            $record = DataObject::get_by_id($treeClass, $id);
1065
        }
1066
1067
        // Then, try getting a deleted record
1068
        if (!$record) {
1069
            $record = Versioned::get_latest_version($treeClass, $id);
1070
        }
1071
1072
        // Set the reading mode back to what it was.
1073
        Versioned::set_reading_mode($currentStage);
1074
1075
        return $record;
1076
    }
1077
1078
    /**
1079
     * {@inheritdoc}
1080
     *
1081
     * @param HTTPRequest $request
1082
     * @return Form
1083
     */
1084
    public function EditForm($request = null)
1085
    {
1086
        // set page ID from request
1087
        if ($request) {
1088
            // Validate id is present
1089
            $id = $request->param('ID');
1090
            if (!isset($id)) {
1091
                $this->httpError(400);
1092
                return null;
1093
            }
1094
            $this->setCurrentPageID($id);
1095
        }
1096
        return $this->getEditForm();
1097
    }
1098
1099
    /**
1100
     * @param int $id
1101
     * @param FieldList $fields
1102
     * @return Form
1103
     */
1104
    public function getEditForm($id = null, $fields = null)
1105
    {
1106
        // Get record
1107
        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...
1108
            $id = $this->currentPageID();
1109
        }
1110
        /** @var SiteTree $record */
1111
        $record = $this->getRecord($id);
1112
1113
        // Check parent form can be generated
1114
        $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...
1115
        if (!$form || !$record) {
1116
            return $form;
1117
        }
1118
1119
        if (!$fields) {
1120
            $fields = $form->Fields();
1121
        }
1122
1123
        // Add extra fields
1124
        $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...
1125
        $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...
1126
        // Necessary for different subsites
1127
        $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...
1128
        $fields->push($liveLinkField = new HiddenField("LiveLink"));
1129
        $fields->push($stageLinkField = new HiddenField("StageLink"));
1130
        $fields->push($archiveWarningMsgField = new HiddenField("ArchiveWarningMessage"));
1131
        $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...
1132
1133
        $archiveWarningMsgField->setValue($this->getArchiveWarningMessage($record));
1134
1135
        // Build preview / live links
1136
        $liveLink = $record->getAbsoluteLiveLink();
1137
        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...
1138
            $liveLinkField->setValue($liveLink);
1139
        }
1140
        if (!$deletedFromStage) {
1141
            $stageLink = Controller::join_links($record->AbsoluteLink(), '?stage=Stage');
1142
            if ($stageLink) {
1143
                $stageLinkField->setValue($stageLink);
1144
            }
1145
        }
1146
1147
        // Added in-line to the form, but plucked into different view by LeftAndMain.Preview.js upon load
1148
        /** @skipUpgrade */
1149
        if ($record instanceof CMSPreviewable && !$fields->fieldByName('SilverStripeNavigator')) {
1150
            $navField = new LiteralField('SilverStripeNavigator', $this->getSilverStripeNavigator());
1151
            $navField->setAllowHTML(true);
1152
            $fields->push($navField);
1153
        }
1154
1155
        // getAllCMSActions can be used to completely redefine the action list
1156
        if ($record->hasMethod('getAllCMSActions')) {
1157
            $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...
1158
        } else {
1159
            $actions = $record->getCMSActions();
1160
1161
            // Find and remove action menus that have no actions.
1162
            if ($actions && $actions->count()) {
1163
                /** @var TabSet $tabset */
1164
                $tabset = $actions->fieldByName('ActionMenus');
1165
                if ($tabset) {
1166
                    /** @var Tab $tab */
1167
                    foreach ($tabset->getChildren() as $tab) {
1168
                        if (!$tab->getChildren()->count()) {
1169
                            $tabset->removeByName($tab->getName());
1170
                        }
1171
                    }
1172
                }
1173
            }
1174
        }
1175
1176
        // Use <button> to allow full jQuery UI styling
1177
        $actionsFlattened = $actions->dataFields();
1178
        if ($actionsFlattened) {
1179
            /** @var FormAction $action */
1180
            foreach ($actionsFlattened as $action) {
1181
                $action->setUseButtonTag(true);
1182
            }
1183
        }
1184
1185
        // TODO Can't merge $FormAttributes in template at the moment
1186
        $form->addExtraClass('center ' . $this->BaseCSSClasses());
1187
        // Set validation exemptions for specific actions
1188
        $form->setValidationExemptActions(array('restore', 'revert', 'deletefromlive', 'delete', 'unpublish', 'rollback', 'doRollback'));
1189
1190
        // Announce the capability so the frontend can decide whether to allow preview or not.
1191
        if ($record instanceof CMSPreviewable) {
1192
            $form->addExtraClass('cms-previewable');
1193
        }
1194
        $form->addExtraClass('fill-height flexbox-area-grow');
1195
1196
        if (!$record->canEdit() || $deletedFromStage) {
1197
            $readonlyFields = $form->Fields()->makeReadonly();
1198
            $form->setFields($readonlyFields);
1199
        }
1200
1201
        $form->Fields()->setForm($form);
1202
1203
        $this->extend('updateEditForm', $form);
1204
1205
        // Use custom reqest handler for LeftAndMain requests;
1206
        // CMS Forms cannot be identified solely by name, but also need ID (and sometimes OtherID)
1207
        $form->setRequestHandler(
1208
            LeftAndMainFormRequestHandler::create($form, [$id])
1209
        );
1210
        return $form;
1211
    }
1212
1213
    public function EmptyForm()
1214
    {
1215
        $fields = new FieldList(
1216
            new LabelField('PageDoesntExistLabel', _t('SilverStripe\\CMS\\Controllers\\CMSMain.PAGENOTEXISTS', "This page doesn't exist"))
1217
        );
1218
        $form = parent::EmptyForm();
1219
        $form->setFields($fields);
1220
        $fields->setForm($form);
1221
        return $form;
1222
    }
1223
1224
    /**
1225
     * Build an archive warning message based on the page's children
1226
     *
1227
     * @param SiteTree $record
1228
     * @return string
1229
     */
1230
    protected function getArchiveWarningMessage($record)
1231
    {
1232
        // Get all page's descendants
1233
        $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...
1234
        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...
1235
            $descendants = [];
1236
        }
1237
1238
        // Get all campaigns that the page and its descendants belong to
1239
        $inChangeSetIDs = ChangeSetItem::get_for_object($record)->column('ChangeSetID');
1240
1241
        foreach ($descendants as $page) {
1242
            $inChangeSetIDs = array_merge($inChangeSetIDs, ChangeSetItem::get_for_object($page)->column('ChangeSetID'));
1243
        }
1244
1245
        if (count($inChangeSetIDs) > 0) {
1246
            $inChangeSets = ChangeSet::get()->filter(['ID' => $inChangeSetIDs, 'State' => ChangeSet::STATE_OPEN]);
1247
        } else {
1248
            $inChangeSets = new ArrayList();
1249
        }
1250
1251
        $numCampaigns = ChangeSet::singleton()->i18n_pluralise($inChangeSets->count());
1252
        $numCampaigns = mb_strtolower($numCampaigns);
1253
1254
        if (count($descendants) > 0 && $inChangeSets->count() > 0) {
1255
            $archiveWarningMsg = _t('SilverStripe\\CMS\\Controllers\\CMSMain.ArchiveWarningWithChildrenAndCampaigns', 'Warning: This page and all of its child pages will be unpublished and automatically removed from their associated {NumCampaigns} before being sent to the archive.\n\nAre you sure you want to proceed?', [ 'NumCampaigns' => $numCampaigns ]);
1256
        } elseif (count($descendants) > 0) {
1257
            $archiveWarningMsg = _t('SilverStripe\\CMS\\Controllers\\CMSMain.ArchiveWarningWithChildren', 'Warning: This page and all of its child pages will be unpublished before being sent to the archive.\n\nAre you sure you want to proceed?');
1258
        } elseif ($inChangeSets->count() > 0) {
1259
            $archiveWarningMsg = _t('SilverStripe\\CMS\\Controllers\\CMSMain.ArchiveWarningWithCampaigns', 'Warning: This page will be unpublished and automatically removed from their associated {NumCampaigns} before being sent to the archive.\n\nAre you sure you want to proceed?', [ 'NumCampaigns' => $numCampaigns ]);
1260
        } else {
1261
            $archiveWarningMsg = _t('SilverStripe\\CMS\\Controllers\\CMSMain.ArchiveWarning', 'Warning: This page will be unpublished before being sent to the archive.\n\nAre you sure you want to proceed?');
1262
        }
1263
1264
        return $archiveWarningMsg;
1265
    }
1266
1267
    /**
1268
     * @param HTTPRequest $request
1269
     * @return string HTML
1270
     */
1271
    public function treeview($request)
1272
    {
1273
        return $this->getResponseNegotiator()->respond($request);
1274
    }
1275
1276
    /**
1277
     * @param HTTPRequest $request
1278
     * @return string HTML
1279
     */
1280
    public function listview($request)
1281
    {
1282
        return $this->getResponseNegotiator()->respond($request);
1283
    }
1284
1285
    /**
1286
     * @return string
1287
     */
1288
    public function ViewState()
1289
    {
1290
        $mode = $this->getRequest()->requestVar('view')
1291
            ?: $this->getRequest()->param('Action');
1292
        switch ($mode) {
1293
            case 'listview':
1294
            case 'treeview':
1295
                return $mode;
1296
            default:
1297
                return 'treeview';
1298
        }
1299
    }
1300
1301
    /**
1302
     * Callback to request the list of page types allowed under a given page instance.
1303
     * Provides a slower but more precise response over SiteTreeHints
1304
     *
1305
     * @param HTTPRequest $request
1306
     * @return HTTPResponse
1307
     */
1308
    public function childfilter($request)
1309
    {
1310
        // Check valid parent specified
1311
        $parentID = $request->requestVar('ParentID');
1312
        $parent = SiteTree::get()->byID($parentID);
1313
        if (!$parent || !$parent->exists()) {
1314
            return $this->httpError(404);
1315
        }
1316
1317
        // Build hints specific to this class
1318
        // Identify disallows and set globals
1319
        $classes = SiteTree::page_type_classes();
1320
        $disallowedChildren = array();
1321
        foreach ($classes as $class) {
1322
            $obj = singleton($class);
1323
            if ($obj instanceof HiddenClass) {
1324
                continue;
1325
            }
1326
1327
            if (!$obj->canCreate(null, array('Parent' => $parent))) {
1328
                $disallowedChildren[] = $class;
1329
            }
1330
        }
1331
1332
        $this->extend('updateChildFilter', $disallowedChildren, $parentID);
1333
        return $this
1334
            ->getResponse()
1335
            ->addHeader('Content-Type', 'application/json; charset=utf-8')
1336
            ->setBody(Convert::raw2json($disallowedChildren));
1337
    }
1338
1339
    /**
1340
     * Safely reconstruct a selected filter from a given set of query parameters
1341
     *
1342
     * @param array $params Query parameters to use
1343
     * @return CMSSiteTreeFilter The filter class, or null if none present
1344
     * @throws InvalidArgumentException if invalid filter class is passed.
1345
     */
1346
    protected function getQueryFilter($params)
1347
    {
1348
        if (empty($params['FilterClass'])) {
1349
            return null;
1350
        }
1351
        $filterClass = $params['FilterClass'];
1352
        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...
1353
            throw new InvalidArgumentException("Invalid filter class passed: {$filterClass}");
1354
        }
1355
        return $filterClass::create($params);
1356
    }
1357
1358
    /**
1359
     * Returns the pages meet a certain criteria as {@see CMSSiteTreeFilter} or the subpages of a parent page
1360
     * defaulting to no filter and show all pages in first level.
1361
     * Doubles as search results, if any search parameters are set through {@link SearchForm()}.
1362
     *
1363
     * @param array $params Search filter criteria
1364
     * @param int $parentID Optional parent node to filter on (can't be combined with other search criteria)
1365
     * @return SS_List
1366
     * @throws InvalidArgumentException if invalid filter class is passed.
1367
     */
1368
    public function getList($params = array(), $parentID = 0)
1369
    {
1370
        if ($filter = $this->getQueryFilter($params)) {
1371
            return $filter->getFilteredPages();
1372
        } else {
1373
            $list = DataList::create($this->stat('tree_class'));
1374
            $parentID = is_numeric($parentID) ? $parentID : 0;
1375
            return $list->filter("ParentID", $parentID);
1376
        }
1377
    }
1378
1379
    /**
1380
     * @return Form
1381
     */
1382
    public function ListViewForm()
1383
    {
1384
        $params = $this->getRequest()->requestVar('q');
1385
        $list = $this->getList($params, $parentID = $this->getRequest()->requestVar('ParentID'));
1386
        $gridFieldConfig = GridFieldConfig::create()->addComponents(
1387
            new GridFieldSortableHeader(),
1388
            new GridFieldDataColumns(),
1389
            new GridFieldPaginator($this->config()->get('page_length'))
1390
        );
1391
        if ($parentID) {
1392
            $linkSpec = $this->Link();
1393
            $linkSpec = $linkSpec . (strstr($linkSpec, '?') ? '&' : '?') . 'ParentID=%d&view=listview';
1394
            $gridFieldConfig->addComponent(
1395
                GridFieldLevelup::create($parentID)
1396
                    ->setLinkSpec($linkSpec)
1397
                    ->setAttributes(array('data-pjax' => 'ListViewForm,Breadcrumbs'))
1398
            );
1399
        }
1400
        $gridField = new GridField('Page', 'Pages', $list, $gridFieldConfig);
1401
        /** @var GridFieldDataColumns $columns */
1402
        $columns = $gridField->getConfig()->getComponentByType('SilverStripe\\Forms\\GridField\\GridFieldDataColumns');
1403
1404
        // Don't allow navigating into children nodes on filtered lists
1405
        $fields = array(
1406
            'getTreeTitle' => _t('SilverStripe\\CMS\\Model\\SiteTree.PAGETITLE', 'Page Title'),
1407
            'singular_name' => _t('SilverStripe\\CMS\\Model\\SiteTree.PAGETYPE', 'Page Type'),
1408
            'LastEdited' => _t('SilverStripe\\CMS\\Model\\SiteTree.LASTUPDATED', 'Last Updated'),
1409
        );
1410
        /** @var GridFieldSortableHeader $sortableHeader */
1411
        $sortableHeader = $gridField->getConfig()->getComponentByType('SilverStripe\\Forms\\GridField\\GridFieldSortableHeader');
1412
        $sortableHeader->setFieldSorting(array('getTreeTitle' => 'Title'));
1413
        $gridField->getState()->ParentID = $parentID;
1414
1415
        if (!$params) {
1416
            $fields = array_merge(array('listChildrenLink' => ''), $fields);
1417
        }
1418
1419
        $columns->setDisplayFields($fields);
1420
        $columns->setFieldCasting(array(
1421
            'Created' => 'DBDatetime->Ago',
1422
            'LastEdited' => 'DBDatetime->FormatFromSettings',
1423
            'getTreeTitle' => 'HTMLFragment'
1424
        ));
1425
1426
        $controller = $this;
1427
        $columns->setFieldFormatting(array(
1428
            'listChildrenLink' => function ($value, &$item) use ($controller) {
1429
                /** @var SiteTree $item */
1430
                $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...
1431
                if ($num) {
1432
                    return sprintf(
1433
                        '<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>',
1434
                        Controller::join_links(
1435
                            $controller->Link(),
1436
                            sprintf("?ParentID=%d&view=listview", (int)$item->ID)
1437
                        ),
1438
                        $num
1439
                    );
1440
                }
1441
            },
1442
            'getTreeTitle' => function ($value, &$item) use ($controller) {
1443
                return sprintf(
1444
                    '<a class="action-detail" href="%s">%s</a>',
1445
                    Controller::join_links(
1446
                        CMSPageEditController::singleton()->Link('show'),
1447
                        (int)$item->ID
1448
                    ),
1449
                    $item->TreeTitle // returns HTML, does its own escaping
1450
                );
1451
            }
1452
        ));
1453
1454
        $negotiator = $this->getResponseNegotiator();
1455
        $listview = Form::create(
1456
            $this,
1457
            'ListViewForm',
1458
            new FieldList($gridField),
1459
            new FieldList()
1460
        )->setHTMLID('Form_ListViewForm');
1461
        $listview->setAttribute('data-pjax-fragment', 'ListViewForm');
1462 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...
1463
            $request = $this->getRequest();
1464
            if ($request->isAjax() && $negotiator) {
1465
                $result = $listview->forTemplate();
1466
                return $negotiator->respond($request, array(
1467
                    'CurrentForm' => function () use ($result) {
1468
                        return $result;
1469
                    }
1470
                ));
1471
            }
1472
        });
1473
1474
        $this->extend('updateListView', $listview);
1475
1476
        $listview->disableSecurityToken();
1477
        return $listview;
1478
    }
1479
1480
    public function currentPageID()
1481
    {
1482
        $id = parent::currentPageID();
1483
1484
        $this->extend('updateCurrentPageID', $id);
1485
1486
        return $id;
1487
    }
1488
1489
    //------------------------------------------------------------------------------------------//
1490
    // Data saving handlers
1491
1492
    /**
1493
     * Save and Publish page handler
1494
     *
1495
     * @param array $data
1496
     * @param Form $form
1497
     * @return HTTPResponse
1498
     * @throws HTTPResponse_Exception
1499
     */
1500
    public function save($data, $form)
1501
    {
1502
        $className = $this->stat('tree_class');
1503
1504
        // Existing or new record?
1505
        $id = $data['ID'];
1506
        if (substr($id, 0, 3) != 'new') {
1507
            /** @var SiteTree $record */
1508
            $record = DataObject::get_by_id($className, $id);
1509
            // Check edit permissions
1510
            if ($record && !$record->canEdit()) {
1511
                return Security::permissionFailure($this);
1512
            }
1513
            if (!$record || !$record->ID) {
1514
                throw new HTTPResponse_Exception("Bad record ID #$id", 404);
1515
            }
1516
        } else {
1517
            if (!$className::singleton()->canCreate()) {
1518
                return Security::permissionFailure($this);
1519
            }
1520
            $record = $this->getNewItem($id, false);
1521
        }
1522
1523
        // Check publishing permissions
1524
        $doPublish = !empty($data['publish']);
1525
        if ($record && $doPublish && !$record->canPublish()) {
1526
            return Security::permissionFailure($this);
1527
        }
1528
1529
        // TODO Coupling to SiteTree
1530
        $record->HasBrokenLink = 0;
1531
        $record->HasBrokenFile = 0;
1532
1533
        if (!$record->ObsoleteClassName) {
1534
            $record->writeWithoutVersion();
1535
        }
1536
1537
        // Update the class instance if necessary
1538
        if (isset($data['ClassName']) && $data['ClassName'] != $record->ClassName) {
1539
            // Replace $record with a new instance of the new class
1540
            $newClassName = $data['ClassName'];
1541
            $record = $record->newClassInstance($newClassName);
1542
        }
1543
1544
        // save form data into record
1545
        $form->saveInto($record);
1546
        $record->write();
1547
1548
        // If the 'Save & Publish' button was clicked, also publish the page
1549
        if ($doPublish) {
1550
            $record->publishRecursive();
1551
            $message = _t(
1552
                'SilverStripe\\CMS\\Controllers\\CMSMain.PUBLISHED',
1553
                "Published '{title}' successfully.",
1554
                ['title' => $record->Title]
1555
            );
1556
        } else {
1557
            $message = _t(
1558
                'SilverStripe\\CMS\\Controllers\\CMSMain.SAVED',
1559
                "Saved '{title}' successfully.",
1560
                ['title' => $record->Title]
1561
            );
1562
        }
1563
1564
        $this->getResponse()->addHeader('X-Status', rawurlencode($message));
1565
        return $this->getResponseNegotiator()->respond($this->getRequest());
1566
    }
1567
1568
    /**
1569
     * @uses LeftAndMainExtension->augmentNewSiteTreeItem()
1570
     *
1571
     * @param int|string $id
1572
     * @param bool $setID
1573
     * @return mixed|DataObject
1574
     * @throws HTTPResponse_Exception
1575
     */
1576
    public function getNewItem($id, $setID = true)
1577
    {
1578
        $parentClass = $this->stat('tree_class');
1579
        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...
1580
1581
        if (!is_a($className, $parentClass, true)) {
1582
            $response = Security::permissionFailure($this);
1583
            if (!$response) {
1584
                $response = $this->getResponse();
1585
            }
1586
            throw new HTTPResponse_Exception($response);
1587
        }
1588
1589
        /** @var SiteTree $newItem */
1590
        $newItem = Injector::inst()->create($className);
1591
        if (!$suffix) {
1592
            $sessionTag = "NewItems." . $parentID . "." . $className;
1593
            if (Session::get($sessionTag)) {
1594
                $suffix = '-' . Session::get($sessionTag);
1595
                Session::set($sessionTag, Session::get($sessionTag) + 1);
1596
            } else {
1597
                Session::set($sessionTag, 1);
1598
            }
1599
1600
                $id = $id . $suffix;
1601
        }
1602
1603
        $newItem->Title = _t(
1604
            'SilverStripe\\CMS\\Controllers\\CMSMain.NEWPAGE',
1605
            "New {pagetype}",
1606
            'followed by a page type title',
1607
            array('pagetype' => singleton($className)->i18n_singular_name())
1608
        );
1609
        $newItem->ClassName = $className;
1610
        $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...
1611
1612
        // DataObject::fieldExists only checks the current class, not the hierarchy
1613
        // This allows the CMS to set the correct sort value
1614
        if ($newItem->castingHelper('Sort')) {
1615
            $newItem->Sort = DB::prepared_query('SELECT MAX("Sort") FROM "SiteTree" WHERE "ParentID" = ?', array($parentID))->value() + 1;
1616
        }
1617
1618
        if ($setID) {
1619
            $newItem->ID = $id;
1620
        }
1621
1622
        # Some modules like subsites add extra fields that need to be set when the new item is created
1623
        $this->extend('augmentNewSiteTreeItem', $newItem);
1624
1625
        return $newItem;
1626
    }
1627
1628
    /**
1629
     * Actually perform the publication step
1630
     *
1631
     * @param Versioned|DataObject $record
1632
     * @return mixed
1633
     */
1634
    public function performPublish($record)
1635
    {
1636
        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...
1637
            return Security::permissionFailure($this);
1638
        }
1639
1640
        $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...
1641
    }
1642
1643
    /**
1644
     * Reverts a page by publishing it to live.
1645
     * Use {@link restorepage()} if you want to restore a page
1646
     * which was deleted from draft without publishing.
1647
     *
1648
     * @uses SiteTree->doRevertToLive()
1649
     *
1650
     * @param array $data
1651
     * @param Form $form
1652
     * @return HTTPResponse
1653
     * @throws HTTPResponse_Exception
1654
     */
1655
    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...
1656
    {
1657
        if (!isset($data['ID'])) {
1658
            throw new HTTPResponse_Exception("Please pass an ID in the form content", 400);
1659
        }
1660
1661
        $id = (int) $data['ID'];
1662
        $restoredPage = Versioned::get_latest_version(SiteTree::class, $id);
1663
        if (!$restoredPage) {
1664
            throw new HTTPResponse_Exception("SiteTree #$id not found", 400);
1665
        }
1666
1667
        /** @var SiteTree $record */
1668
        $record = Versioned::get_one_by_stage('SilverStripe\\CMS\\Model\\SiteTree', 'Live', array(
1669
            '"SiteTree_Live"."ID"' => $id
1670
        ));
1671
1672
        // a user can restore a page without publication rights, as it just adds a new draft state
1673
        // (this action should just be available when page has been "deleted from draft")
1674
        if ($record && !$record->canEdit()) {
1675
            return Security::permissionFailure($this);
1676
        }
1677
        if (!$record || !$record->ID) {
1678
            throw new HTTPResponse_Exception("Bad record ID #$id", 404);
1679
        }
1680
1681
        $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...
1682
1683
        $this->getResponse()->addHeader(
1684
            'X-Status',
1685
            rawurlencode(_t(
1686
                'SilverStripe\\CMS\\Controllers\\CMSMain.RESTORED',
1687
                "Restored '{title}' successfully",
1688
                'Param %s is a title',
1689
                array('title' => $record->Title)
1690
            ))
1691
        );
1692
1693
        return $this->getResponseNegotiator()->respond($this->getRequest());
1694
    }
1695
1696
    /**
1697
     * Delete the current page from draft stage.
1698
     *
1699
     * @see deletefromlive()
1700
     *
1701
     * @param array $data
1702
     * @param Form $form
1703
     * @return HTTPResponse
1704
     * @throws HTTPResponse_Exception
1705
     */
1706 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...
1707
    {
1708
        $id = $data['ID'];
1709
        $record = SiteTree::get()->byID($id);
1710
        if ($record && !$record->canDelete()) {
1711
            return Security::permissionFailure();
1712
        }
1713
        if (!$record || !$record->ID) {
1714
            throw new HTTPResponse_Exception("Bad record ID #$id", 404);
1715
        }
1716
1717
        // Delete record
1718
        $record->delete();
1719
1720
        $this->getResponse()->addHeader(
1721
            'X-Status',
1722
            rawurlencode(sprintf(_t('SilverStripe\\CMS\\Controllers\\CMSMain.REMOVEDPAGEFROMDRAFT', "Removed '%s' from the draft site"), $record->Title))
1723
        );
1724
1725
        // Even if the record has been deleted from stage and live, it can be viewed in "archive mode"
1726
        return $this->getResponseNegotiator()->respond($this->getRequest());
1727
    }
1728
1729
    /**
1730
     * Delete this page from both live and stage
1731
     *
1732
     * @param array $data
1733
     * @param Form $form
1734
     * @return HTTPResponse
1735
     * @throws HTTPResponse_Exception
1736
     */
1737 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...
1738
    {
1739
        $id = $data['ID'];
1740
        /** @var SiteTree $record */
1741
        $record = SiteTree::get()->byID($id);
1742
        if (!$record || !$record->exists()) {
1743
            throw new HTTPResponse_Exception("Bad record ID #$id", 404);
1744
        }
1745
        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...
1746
            return Security::permissionFailure();
1747
        }
1748
1749
        // Archive record
1750
        $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...
1751
1752
        $this->getResponse()->addHeader(
1753
            'X-Status',
1754
            rawurlencode(sprintf(_t('SilverStripe\\CMS\\Controllers\\CMSMain.ARCHIVEDPAGE', "Archived page '%s'"), $record->Title))
1755
        );
1756
1757
        // Even if the record has been deleted from stage and live, it can be viewed in "archive mode"
1758
        return $this->getResponseNegotiator()->respond($this->getRequest());
1759
    }
1760
1761
    public function publish($data, $form)
1762
    {
1763
        $data['publish'] = '1';
1764
1765
        return $this->save($data, $form);
1766
    }
1767
1768
    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...
1769
    {
1770
        $className = $this->stat('tree_class');
1771
        /** @var SiteTree $record */
1772
        $record = DataObject::get_by_id($className, $data['ID']);
1773
1774
        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...
1775
            return Security::permissionFailure($this);
1776
        }
1777
        if (!$record || !$record->ID) {
1778
            throw new HTTPResponse_Exception("Bad record ID #" . (int)$data['ID'], 404);
1779
        }
1780
1781
        $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...
1782
1783
        $this->getResponse()->addHeader(
1784
            'X-Status',
1785
            rawurlencode(_t('SilverStripe\\CMS\\Controllers\\CMSMain.REMOVEDPAGE', "Removed '{title}' from the published site", array('title' => $record->Title)))
1786
        );
1787
1788
        return $this->getResponseNegotiator()->respond($this->getRequest());
1789
    }
1790
1791
    /**
1792
     * @return HTTPResponse
1793
     */
1794
    public function rollback()
1795
    {
1796
        return $this->doRollback(array(
1797
            'ID' => $this->currentPageID(),
1798
            'Version' => $this->getRequest()->param('VersionID')
1799
        ), 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...
1800
    }
1801
1802
    /**
1803
     * Rolls a site back to a given version ID
1804
     *
1805
     * @param array $data
1806
     * @param Form $form
1807
     * @return HTTPResponse
1808
     */
1809
    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...
1810
    {
1811
        $this->extend('onBeforeRollback', $data['ID']);
1812
1813
        $id = (isset($data['ID'])) ? (int) $data['ID'] : null;
1814
        $version = (isset($data['Version'])) ? (int) $data['Version'] : null;
1815
1816
        /** @var DataObject|Versioned $record */
1817
        $record = DataObject::get_by_id($this->stat('tree_class'), $id);
1818
        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...
1819
            return Security::permissionFailure($this);
1820
        }
1821
1822
        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...
1823
            $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...
1824
            $message = _t(
1825
                'SilverStripe\\CMS\\Controllers\\CMSMain.ROLLEDBACKVERSIONv2',
1826
                "Rolled back to version #%d.",
1827
                array('version' => $data['Version'])
1828
            );
1829
        } else {
1830
            $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...
1831
            $message = _t(
1832
                'SilverStripe\\CMS\\Controllers\\CMSMain.ROLLEDBACKPUBv2',
1833
                "Rolled back to published version."
1834
            );
1835
        }
1836
1837
        $this->getResponse()->addHeader('X-Status', rawurlencode($message));
1838
1839
        // Can be used in different contexts: In normal page edit view, in which case the redirect won't have any effect.
1840
        // Or in history view, in which case a revert causes the CMS to re-load the edit view.
1841
        // The X-Pjax header forces a "full" content refresh on redirect.
1842
        $url = Controller::join_links(CMSPageEditController::singleton()->Link('show'), $record->ID);
1843
        $this->getResponse()->addHeader('X-ControllerURL', $url);
1844
        $this->getRequest()->addHeader('X-Pjax', 'Content');
1845
        $this->getResponse()->addHeader('X-Pjax', 'Content');
1846
1847
        return $this->getResponseNegotiator()->respond($this->getRequest());
1848
    }
1849
1850
    /**
1851
     * Batch Actions Handler
1852
     */
1853
    public function batchactions()
1854
    {
1855
        return new CMSBatchActionHandler($this, 'batchactions');
1856
    }
1857
1858
    public function BatchActionParameters()
1859
    {
1860
        $batchActions = CMSBatchActionHandler::config()->batch_actions;
1861
1862
        $forms = array();
1863
        foreach ($batchActions as $urlSegment => $batchAction) {
1864
            $SNG_action = singleton($batchAction);
1865
            if ($SNG_action->canView() && $fieldset = $SNG_action->getParameterFields()) {
1866
                $formHtml = '';
1867
                /** @var FormField $field */
1868
                foreach ($fieldset as $field) {
1869
                    $formHtml .= $field->Field();
1870
                }
1871
                $forms[$urlSegment] = $formHtml;
1872
            }
1873
        }
1874
        $pageHtml = '';
1875
        foreach ($forms as $urlSegment => $html) {
1876
            $pageHtml .= "<div class=\"params\" id=\"BatchActionParameters_$urlSegment\">$html</div>\n\n";
1877
        }
1878
        return new LiteralField("BatchActionParameters", '<div id="BatchActionParameters" style="display:none">'.$pageHtml.'</div>');
1879
    }
1880
    /**
1881
     * Returns a list of batch actions
1882
     */
1883
    public function BatchActionList()
1884
    {
1885
        return $this->batchactions()->batchActionList();
1886
    }
1887
1888
    public function publishall($request)
1889
    {
1890
        if (!Permission::check('ADMIN')) {
1891
            return Security::permissionFailure($this);
1892
        }
1893
1894
        increase_time_limit_to();
1895
        increase_memory_limit_to();
1896
1897
        $response = "";
1898
1899
        if (isset($this->requestParams['confirm'])) {
1900
            // Protect against CSRF on destructive action
1901
            if (!SecurityToken::inst()->checkRequest($request)) {
1902
                return $this->httpError(400);
1903
            }
1904
1905
            $start = 0;
1906
            $pages = SiteTree::get()->limit("$start,30");
1907
            $count = 0;
1908
            while ($pages) {
1909
                /** @var SiteTree $page */
1910
                foreach ($pages as $page) {
1911
                    if ($page && !$page->canPublish()) {
1912
                        return Security::permissionFailure($this);
1913
                    }
1914
1915
                    $page->publishRecursive();
1916
                    $page->destroy();
1917
                    unset($page);
1918
                    $count++;
1919
                    $response .= "<li>$count</li>";
1920
                }
1921
                if ($pages->count() > 29) {
1922
                    $start += 30;
1923
                    $pages = SiteTree::get()->limit("$start,30");
1924
                } else {
1925
                    break;
1926
                }
1927
            }
1928
            $response .= _t('SilverStripe\\CMS\\Controllers\\CMSMain.PUBPAGES', "Done: Published {count} pages", array('count' => $count));
1929
        } else {
1930
            $token = SecurityToken::inst();
1931
            $fields = new FieldList();
1932
            $token->updateFieldSet($fields);
1933
            $tokenField = $fields->first();
1934
            $tokenHtml = ($tokenField) ? $tokenField->FieldHolder() : '';
1935
            $publishAllDescription = _t(
1936
                'SilverStripe\\CMS\\Controllers\\CMSMain.PUBALLFUN2',
1937
                'Pressing this button will do the equivalent of going to every page and pressing "publish".  '
1938
                . 'It\'s intended to be used after there have been massive edits of the content, such as when '
1939
                . 'the site was first built.'
1940
            );
1941
            $response .= '<h1>' . _t('SilverStripe\\CMS\\Controllers\\CMSMain.PUBALLFUN', '"Publish All" functionality') . '</h1>
1942
				<p>' . $publishAllDescription . '</p>
1943
				<form method="post" action="publishall">
1944
					<input type="submit" name="confirm" value="'
1945
                    . _t('SilverStripe\\CMS\\Controllers\\CMSMain.PUBALLCONFIRM', "Please publish every page in the site, copying content stage to live", 'Confirmation button') .'" />'
1946
                    . $tokenHtml .
1947
                '</form>';
1948
        }
1949
1950
        return $response;
1951
    }
1952
1953
    /**
1954
     * Restore a completely deleted page from the SiteTree_versions table.
1955
     *
1956
     * @param array $data
1957
     * @param Form $form
1958
     * @return HTTPResponse
1959
     */
1960
    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...
1961
    {
1962
        if (!isset($data['ID']) || !is_numeric($data['ID'])) {
1963
            return new HTTPResponse("Please pass an ID in the form content", 400);
1964
        }
1965
1966
        $id = (int)$data['ID'];
1967
        /** @var SiteTree $restoredPage */
1968
        $restoredPage = Versioned::get_latest_version(SiteTree::class, $id);
1969
        if (!$restoredPage) {
1970
            return new HTTPResponse("SiteTree #$id not found", 400);
1971
        }
1972
1973
        $restoredPage = $restoredPage->doRestoreToStage();
1974
1975
        $this->getResponse()->addHeader(
1976
            'X-Status',
1977
            rawurlencode(_t(
1978
                'SilverStripe\\CMS\\Controllers\\CMSMain.RESTORED',
1979
                "Restored '{title}' successfully",
1980
                array('title' => $restoredPage->Title)
1981
            ))
1982
        );
1983
1984
        return $this->getResponseNegotiator()->respond($this->getRequest());
1985
    }
1986
1987
    public function duplicate($request)
1988
    {
1989
        // Protect against CSRF on destructive action
1990
        if (!SecurityToken::inst()->checkRequest($request)) {
1991
            return $this->httpError(400);
1992
        }
1993
1994
        if (($id = $this->urlParams['ID']) && is_numeric($id)) {
1995
            /** @var SiteTree $page */
1996
            $page = SiteTree::get()->byID($id);
1997 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...
1998
                return Security::permissionFailure($this);
1999
            }
2000
            if (!$page || !$page->ID) {
2001
                throw new HTTPResponse_Exception("Bad record ID #$id", 404);
2002
            }
2003
2004
            $newPage = $page->duplicate();
2005
2006
            // ParentID can be hard-set in the URL.  This is useful for pages with multiple parents
2007
            if (isset($_GET['parentID']) && is_numeric($_GET['parentID'])) {
2008
                $newPage->ParentID = $_GET['parentID'];
2009
                $newPage->write();
2010
            }
2011
2012
            $this->getResponse()->addHeader(
2013
                'X-Status',
2014
                rawurlencode(_t(
2015
                    'SilverStripe\\CMS\\Controllers\\CMSMain.DUPLICATED',
2016
                    "Duplicated '{title}' successfully",
2017
                    array('title' => $newPage->Title)
2018
                ))
2019
            );
2020
            $url = Controller::join_links(CMSPageEditController::singleton()->Link('show'), $newPage->ID);
2021
            $this->getResponse()->addHeader('X-ControllerURL', $url);
2022
            $this->getRequest()->addHeader('X-Pjax', 'Content');
2023
            $this->getResponse()->addHeader('X-Pjax', 'Content');
2024
2025
            return $this->getResponseNegotiator()->respond($this->getRequest());
2026
        } else {
2027
            return new HTTPResponse("CMSMain::duplicate() Bad ID: '$id'", 400);
2028
        }
2029
    }
2030
2031
    public function duplicatewithchildren($request)
2032
    {
2033
        // Protect against CSRF on destructive action
2034
        if (!SecurityToken::inst()->checkRequest($request)) {
2035
            return $this->httpError(400);
2036
        }
2037
        increase_time_limit_to();
2038
        if (($id = $this->urlParams['ID']) && is_numeric($id)) {
2039
            /** @var SiteTree $page */
2040
            $page = SiteTree::get()->byID($id);
2041 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...
2042
                return Security::permissionFailure($this);
2043
            }
2044
            if (!$page || !$page->ID) {
2045
                throw new HTTPResponse_Exception("Bad record ID #$id", 404);
2046
            }
2047
2048
            $newPage = $page->duplicateWithChildren();
2049
2050
            $this->getResponse()->addHeader(
2051
                'X-Status',
2052
                rawurlencode(_t(
2053
                    'SilverStripe\\CMS\\Controllers\\CMSMain.DUPLICATEDWITHCHILDREN',
2054
                    "Duplicated '{title}' and children successfully",
2055
                    array('title' => $newPage->Title)
2056
                ))
2057
            );
2058
            $url = Controller::join_links(CMSPageEditController::singleton()->Link('show'), $newPage->ID);
2059
            $this->getResponse()->addHeader('X-ControllerURL', $url);
2060
            $this->getRequest()->addHeader('X-Pjax', 'Content');
2061
            $this->getResponse()->addHeader('X-Pjax', 'Content');
2062
2063
            return $this->getResponseNegotiator()->respond($this->getRequest());
2064
        } else {
2065
            return new HTTPResponse("CMSMain::duplicatewithchildren() Bad ID: '$id'", 400);
2066
        }
2067
    }
2068
2069
    public function providePermissions()
2070
    {
2071
        $title = CMSPagesController::menu_title();
2072
        return array(
2073
            "CMS_ACCESS_CMSMain" => array(
2074
                'name' => _t('SilverStripe\\CMS\\Controllers\\CMSMain.ACCESS', "Access to '{title}' section", array('title' => $title)),
2075
                'category' => _t('SilverStripe\\Security\\Permission.CMS_ACCESS_CATEGORY', 'CMS Access'),
2076
                'help' => _t(
2077
                    'SilverStripe\\CMS\\Controllers\\CMSMain.ACCESS_HELP',
2078
                    '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".'
2079
                ),
2080
                'sort' => -99 // below "CMS_ACCESS_LeftAndMain", but above everything else
2081
            )
2082
        );
2083
    }
2084
2085
    /**
2086
     * Get title for root CMS node
2087
     *
2088
     * @return string
2089
     */
2090
    protected function getCMSTreeTitle()
2091
    {
2092
        $rootTitle = SiteConfig::current_site_config()->Title;
2093
        $this->extend('updateCMSTreeTitle', $rootTitle);
2094
        return $rootTitle;
2095
    }
2096
}
2097