Completed
Push — master ( 83e094...393d39 )
by Damian
02:03 queued 01:58
created

CMSMain::SearchForm()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 75
Code Lines 44

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 75
rs 9
c 0
b 0
f 0
cc 2
eloc 44
nc 2
nop 0

How to fix   Long Method   

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\ORM\CMSPreviewable;
8
use SilverStripe\Admin\LeftAndMain;
9
use SilverStripe\CMS\BatchActions\CMSBatchAction_Archive;
10
use SilverStripe\CMS\BatchActions\CMSBatchAction_Publish;
11
use SilverStripe\CMS\BatchActions\CMSBatchAction_Restore;
12
use SilverStripe\CMS\BatchActions\CMSBatchAction_Unpublish;
13
use SilverStripe\CMS\Model\CurrentPageIdentifier;
14
use SilverStripe\CMS\Model\RedirectorPage;
15
use SilverStripe\CMS\Model\SiteTree;
16
use SilverStripe\Control\Controller;
17
use SilverStripe\Control\Director;
18
use SilverStripe\Control\Session;
19
use SilverStripe\Control\HTTPRequest;
20
use SilverStripe\Control\HTTPResponse;
21
use SilverStripe\Control\HTTPResponse_Exception;
22
use SilverStripe\Core\Convert;
23
use SilverStripe\Core\Injector\Injector;
24
use Psr\SimpleCache\CacheInterface;
25
use SilverStripe\Forms\DateField;
26
use SilverStripe\Forms\DropdownField;
27
use SilverStripe\Forms\FieldGroup;
28
use SilverStripe\Forms\FieldList;
29
use SilverStripe\Forms\Form;
30
use SilverStripe\Forms\FormAction;
31
use SilverStripe\Forms\FormField;
32
use SilverStripe\Forms\GridField\GridField;
33
use SilverStripe\Forms\GridField\GridFieldConfig;
34
use SilverStripe\Forms\GridField\GridFieldDataColumns;
35
use SilverStripe\Forms\GridField\GridFieldLevelup;
36
use SilverStripe\Forms\GridField\GridFieldPaginator;
37
use SilverStripe\Forms\GridField\GridFieldSortableHeader;
38
use SilverStripe\Forms\HiddenField;
39
use SilverStripe\Forms\LabelField;
40
use SilverStripe\Forms\LiteralField;
41
use SilverStripe\Forms\RequiredFields;
42
use SilverStripe\Forms\ResetFormAction;
43
use SilverStripe\Forms\TabSet;
44
use SilverStripe\Forms\TextField;
45
use SilverStripe\ORM\ArrayList;
46
use SilverStripe\ORM\DataList;
47
use SilverStripe\ORM\DataObject;
48
use SilverStripe\ORM\DB;
49
use SilverStripe\ORM\FieldType\DBHTMLText;
50
use SilverStripe\ORM\HiddenClass;
51
use SilverStripe\ORM\SS_List;
52
use SilverStripe\ORM\ValidationResult;
53
use SilverStripe\Versioned\Versioned;
54
use SilverStripe\Security\Member;
55
use SilverStripe\Security\Permission;
56
use SilverStripe\Security\PermissionProvider;
57
use SilverStripe\Security\Security;
58
use SilverStripe\Security\SecurityToken;
59
use SilverStripe\View\ArrayData;
60
use SilverStripe\View\Requirements;
61
use Translatable;
62
use InvalidArgumentException;
63
64
/**
65
 * The main "content" area of the CMS.
66
 *
67
 * This class creates a 2-frame layout - left-tree and right-form - to sit beneath the main
68
 * admin menu.
69
 *
70
 * @todo Create some base classes to contain the generic functionality that will be replicated.
71
 *
72
 * @mixin LeftAndMainPageIconsExtension
73
 */
74
class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionProvider
75
{
76
77
    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...
78
79
    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...
80
81
    // Maintain a lower priority than other administration sections
82
    // so that Director does not think they are actions of CMSMain
83
    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...
84
85
    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...
86
87
    private static $menu_icon_class = 'font-icon-sitemap';
88
89
    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...
90
91
    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...
92
93
    private static $subitem_class = Member::class;
94
95
    private static $session_namespace = 'SilverStripe\\CMS\\Controllers\\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...
96
97
    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...
98
99
    /**
100
     * Amount of results showing on a single page.
101
     *
102
     * @config
103
     * @var int
104
     */
105
    private static $page_length = 15;
106
107
    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...
108
        'archive',
109
        'deleteitems',
110
        'DeleteItemsForm',
111
        'dialog',
112
        'duplicate',
113
        'duplicatewithchildren',
114
        'publishall',
115
        'publishitems',
116
        'PublishItemsForm',
117
        'submit',
118
        'EditForm',
119
        'SearchForm',
120
        'SiteTreeAsUL',
121
        'getshowdeletedsubtree',
122
        'batchactions',
123
        'treeview',
124
        'listview',
125
        'ListViewForm',
126
        'childfilter',
127
    );
128
129
    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...
130
        'TreeIsFiltered' => 'Boolean',
131
        'AddForm' => 'HTMLFragment',
132
        'LinkPages' => 'Text',
133
        'Link' => 'Text',
134
        'ListViewForm' => 'HTMLFragment',
135
        'ExtraTreeTools' => 'HTMLFragment',
136
        'PageList' => 'HTMLFragment',
137
        'PageListSidebar' => 'HTMLFragment',
138
        'SiteTreeHints' => 'HTMLFragment',
139
        'SecurityID' => 'Text',
140
        'SiteTreeAsUL' => 'HTMLFragment',
141
    );
142
143
    protected function init()
144
    {
145
        // set reading lang
146
        if (SiteTree::has_extension('Translatable') && !$this->getRequest()->isAjax()) {
147
            Translatable::choose_site_locale(array_keys(Translatable::get_existing_content_languages('SilverStripe\\CMS\\Model\\SiteTree')));
148
        }
149
150
        parent::init();
151
152
        Requirements::javascript(CMS_DIR . '/client/dist/js/bundle.js');
153
        Requirements::javascript(CMS_DIR . '/client/dist/js/SilverStripeNavigator.js');
154
        Requirements::css(CMS_DIR . '/client/dist/styles/bundle.css');
155
        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...
156
        Requirements::add_i18n_javascript(CMS_DIR . '/client/lang', false, true);
157
158
        CMSBatchActionHandler::register('restore', CMSBatchAction_Restore::class);
159
        CMSBatchActionHandler::register('archive', CMSBatchAction_Archive::class);
160
        CMSBatchActionHandler::register('unpublish', CMSBatchAction_Unpublish::class);
161
        CMSBatchActionHandler::register('publish', CMSBatchAction_Publish::class);
162
    }
163
164
    public function index($request)
165
    {
166
        // In case we're not showing a specific record, explicitly remove any session state,
167
        // to avoid it being highlighted in the tree, and causing an edit form to show.
168
        if (!$request->param('Action')) {
169
            $this->setCurrentPageID(null);
170
        }
171
172
        return parent::index($request);
173
    }
174
175
    public function getResponseNegotiator()
176
    {
177
        $negotiator = parent::getResponseNegotiator();
178
179
        // ListViewForm
180
        $negotiator->setCallback('ListViewForm', function () {
181
            return $this->ListViewForm()->forTemplate();
182
        });
183
184
        // PageList view
185
        $negotiator->setCallback('Content-PageList', function () {
186
            return $this->PageList()->forTemplate();
187
        });
188
189
        // PageList view for edit controller
190
        $negotiator->setCallback('Content-PageList-Sidebar', function () {
191
            return $this->PageListSidebar()->forTemplate();
192
        });
193
194
        return $negotiator;
195
    }
196
197
    /**
198
     * Get pages listing area
199
     *
200
     * @return DBHTMLText
201
     */
202
    public function PageList()
203
    {
204
        return $this->renderWith($this->getTemplatesWithSuffix('_PageList'));
205
    }
206
207
    /**
208
     * Page list view for edit-form
209
     *
210
     * @return DBHTMLText
211
     */
212
    public function PageListSidebar()
213
    {
214
        return $this->renderWith($this->getTemplatesWithSuffix('_PageList_Sidebar'));
215
    }
216
217
    /**
218
     * If this is set to true, the "switchView" context in the
219
     * template is shown, with links to the staging and publish site.
220
     *
221
     * @return boolean
222
     */
223
    public function ShowSwitchView()
224
    {
225
        return true;
226
    }
227
228
    /**
229
     * Overloads the LeftAndMain::ShowView. Allows to pass a page as a parameter, so we are able
230
     * to switch view also for archived versions.
231
     *
232
     * @param SiteTree $page
233
     * @return array
234
     */
235
    public function SwitchView($page = null)
236
    {
237
        if (!$page) {
238
            $page = $this->currentPage();
239
        }
240
241
        if ($page) {
242
            $nav = SilverStripeNavigator::get_for_record($page);
243
            return $nav['items'];
244
        }
245
    }
246
247
    //------------------------------------------------------------------------------------------//
248
    // Main controllers
249
250
    //------------------------------------------------------------------------------------------//
251
    // Main UI components
252
253
    /**
254
     * Override {@link LeftAndMain} Link to allow blank URL segment for CMSMain.
255
     *
256
     * @param string|null $action Action to link to.
257
     * @return string
258
     */
259
    public function Link($action = null)
260
    {
261
        $link = Controller::join_links(
262
            AdminRootController::admin_url(),
263
            $this->stat('url_segment'), // in case we want to change the segment
264
            '/', // trailing slash needed if $action is null!
265
            "$action"
266
        );
267
        $this->extend('updateLink', $link);
268
        return $link;
269
    }
270
271
    public function LinkPages()
272
    {
273
        return CMSPagesController::singleton()->Link();
274
    }
275
276
    public function LinkPagesWithSearch()
277
    {
278
        return $this->LinkWithSearch($this->LinkPages());
279
    }
280
281
    /**
282
     * Get link to tree view
283
     *
284
     * @return string
285
     */
286
    public function LinkTreeView()
287
    {
288
        // Tree view is just default link to main pages section (no /treeview suffix)
289
        return $this->LinkWithSearch(CMSMain::singleton()->Link());
290
    }
291
292
    /**
293
     * Get link to list view
294
     *
295
     * @return string
296
     */
297
    public function LinkListView()
298
    {
299
        // Note : Force redirect to top level page controller
300
        return $this->LinkWithSearch(CMSMain::singleton()->Link('listview'));
301
    }
302
303 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...
304
    {
305
        if (!$id) {
306
            $id = $this->currentPageID();
307
        }
308
        return $this->LinkWithSearch(
309
            Controller::join_links(CMSPageEditController::singleton()->Link('show'), $id)
310
        );
311
    }
312
313 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...
314
    {
315
        if ($id = $this->currentPageID()) {
316
            return $this->LinkWithSearch(
317
                Controller::join_links(CMSPageSettingsController::singleton()->Link('show'), $id)
318
            );
319
        } else {
320
            return null;
321
        }
322
    }
323
324 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...
325
    {
326
        if ($id = $this->currentPageID()) {
327
            return $this->LinkWithSearch(
328
                Controller::join_links(CMSPageHistoryController::singleton()->Link('show'), $id)
329
            );
330
        } else {
331
            return null;
332
        }
333
    }
334
335
    public function LinkWithSearch($link)
336
    {
337
        // Whitelist to avoid side effects
338
        $params = array(
339
            'q' => (array)$this->getRequest()->getVar('q'),
340
            'ParentID' => $this->getRequest()->getVar('ParentID')
341
        );
342
        $link = Controller::join_links(
343
            $link,
344
            array_filter(array_values($params)) ? '?' . http_build_query($params) : null
345
        );
346
        $this->extend('updateLinkWithSearch', $link);
347
        return $link;
348
    }
349
350
    public function LinkPageAdd($extra = null, $placeholders = null)
351
    {
352
        $link = CMSPageAddController::singleton()->Link();
353
        $this->extend('updateLinkPageAdd', $link);
354
355
        if ($extra) {
356
            $link = Controller::join_links($link, $extra);
357
        }
358
359
        if ($placeholders) {
360
            $link .= (strpos($link, '?') === false ? "?$placeholders" : "&$placeholders");
361
        }
362
363
        return $link;
364
    }
365
366
    /**
367
     * @return string
368
     */
369
    public function LinkPreview()
370
    {
371
        $record = $this->getRecord($this->currentPageID());
372
        $baseLink = Director::absoluteBaseURL();
373
        if ($record && $record instanceof SiteTree) {
374
            // if we are an external redirector don't show a link
375
            if ($record instanceof RedirectorPage && $record->RedirectionType == 'External') {
376
                $baseLink = false;
377
            } else {
378
                $baseLink = $record->Link('?stage=Stage');
379
            }
380
        }
381
        return $baseLink;
382
    }
383
384
    /**
385
     * Return the entire site tree as a nested set of ULs
386
     */
387
    public function SiteTreeAsUL()
388
    {
389
        // Pre-cache sitetree version numbers for querying efficiency
390
        Versioned::prepopulate_versionnumber_cache(SiteTree::class, "Stage");
391
        Versioned::prepopulate_versionnumber_cache(SiteTree::class, "Live");
392
        $html = $this->getSiteTreeFor($this->stat('tree_class'));
393
394
        $this->extend('updateSiteTreeAsUL', $html);
395
396
        return $html;
397
    }
398
399
    /**
400
     * @return boolean
401
     */
402
    public function TreeIsFiltered()
403
    {
404
        $query = $this->getRequest()->getVar('q');
405
406
        if (!$query || (count($query) === 1 && isset($query['FilterClass']) && $query['FilterClass'] === 'SilverStripe\\CMS\\Controllers\\CMSSiteTreeFilter_Search')) {
407
            return false;
408
        }
409
410
        return true;
411
    }
412
413
    public function ExtraTreeTools()
414
    {
415
        $html = '';
416
        $this->extend('updateExtraTreeTools', $html);
417
        return $html;
418
    }
419
420
    /**
421
     * Returns a Form for page searching for use in templates.
422
     *
423
     * Can be modified from a decorator by a 'updateSearchForm' method
424
     *
425
     * @return Form
426
     */
427
    public function SearchForm()
428
    {
429
        // Create the fields
430
        $content = new TextField('q[Term]', _t('CMSSearch.FILTERLABELTEXT', 'Search'));
431
        $dateFrom = new DateField(
432
            'q[LastEditedFrom]',
433
            _t('CMSSearch.FILTERDATEFROM', 'From')
434
        );
435
        $dateTo = new DateField(
436
            'q[LastEditedTo]',
437
            _t('CMSSearch.FILTERDATETO', 'To')
438
        );
439
        $pageFilter = new DropdownField(
440
            'q[FilterClass]',
441
            _t('CMSMain.PAGES', 'Page status'),
442
            CMSSiteTreeFilter::get_all_filters()
443
        );
444
        $pageClasses = new DropdownField(
445
            'q[ClassName]',
446
            _t('CMSMain.PAGETYPEOPT', 'Page type', 'Dropdown for limiting search to a page type'),
447
            $this->getPageTypes()
448
        );
449
        $pageClasses->setEmptyString(_t('CMSMain.PAGETYPEANYOPT', 'Any'));
450
451
        // Group the Datefields
452
        $dateGroup = new FieldGroup(
453
            $dateFrom,
454
            $dateTo
455
        );
456
        $dateGroup->setTitle(_t('CMSSearch.PAGEFILTERDATEHEADING', 'Last edited'));
457
458
        // view mode
459
        $viewMode = HiddenField::create('view', false, $this->ViewState());
460
461
        // Create the Field list
462
        $fields = new FieldList(
463
            $content,
464
            $pageFilter,
465
            $pageClasses,
466
            $dateGroup,
467
            $viewMode
468
        );
469
470
        // Create the Search and Reset action
471
        $actions = new FieldList(
472
            FormAction::create('doSearch', _t('CMSMain_left_ss.APPLY_FILTER', 'Search'))
473
                ->addExtraClass('btn btn-primary'),
474
            ResetFormAction::create('clear', _t('CMSMain_left_ss.CLEAR_FILTER', 'Clear'))
475
                ->addExtraClass('btn btn-secondary')
476
        );
477
478
        // Use <button> to allow full jQuery UI styling on the all of the Actions
479
        /** @var FormAction $action */
480
        foreach ($actions->dataFields() as $action) {
481
            /** @var FormAction $action */
482
            $action->setUseButtonTag(true);
483
        }
484
485
        // Create the form
486
        /** @skipUpgrade */
487
        $form = Form::create($this, 'SearchForm', $fields, $actions)
488
            ->addExtraClass('cms-search-form')
489
            ->setFormMethod('GET')
490
            ->setFormAction($this->Link())
491
            ->disableSecurityToken()
492
            ->unsetValidator();
493
494
        // Load the form with previously sent search data
495
        $form->loadDataFrom($this->getRequest()->getVars());
496
497
        // Allow decorators to modify the form
498
        $this->extend('updateSearchForm', $form);
499
500
        return $form;
501
    }
502
503
    /**
504
     * Returns a sorted array suitable for a dropdown with pagetypes and their translated name
505
     *
506
     * @return array
507
     */
508
    protected function getPageTypes()
509
    {
510
        $pageTypes = array();
511
        foreach (SiteTree::page_type_classes() as $pageTypeClass) {
512
            $pageTypes[$pageTypeClass] = SiteTree::singleton($pageTypeClass)->i18n_singular_name();
513
        }
514
        asort($pageTypes);
515
        return $pageTypes;
516
    }
517
518
    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...
519
    {
520
        return $this->getsubtree($this->getRequest());
521
    }
522
523
    /**
524
     * @param bool $unlinked
525
     * @return ArrayList
526
     */
527
    public function Breadcrumbs($unlinked = false)
528
    {
529
        $items = parent::Breadcrumbs($unlinked);
530
531
        if ($items->count() > 1) {
532
            // Specific to the SiteTree admin section, we never show the cms section and current
533
            // page in the same breadcrumbs block.
534
            $items->shift();
535
        }
536
537
        return $items;
538
    }
539
540
    /**
541
     * Create serialized JSON string with site tree hints data to be injected into
542
     * 'data-hints' attribute of root node of jsTree.
543
     *
544
     * @return string Serialized JSON
545
     */
546
    public function SiteTreeHints()
547
    {
548
        $classes = SiteTree::page_type_classes();
549
550
        $cacheCanCreate = array();
551
        foreach ($classes as $class) {
552
            $cacheCanCreate[$class] = singleton($class)->canCreate();
553
        }
554
555
        // Generate basic cache key. Too complex to encompass all variations
556
        $cache = Injector::inst()->get(CacheInterface::class . '.CMSMain_SiteTreeHints');
557
        $cacheKey = md5(implode('_', array(Member::currentUserID(), implode(',', $cacheCanCreate), implode(',', $classes))));
558
        if ($this->getRequest()->getVar('flush')) {
559
            $cache->clear();
560
        }
561
        $json = $cache->get($cacheKey);
562
        if (!$json) {
563
            $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...
564
            $def['Root']['disallowedChildren'] = array();
565
566
            // Contains all possible classes to support UI controls listing them all,
567
            // such as the "add page here" context menu.
568
            $def['All'] = array();
569
570
            // Identify disallows and set globals
571
            foreach ($classes as $class) {
572
                $obj = singleton($class);
573
                if ($obj instanceof HiddenClass) {
574
                    continue;
575
                }
576
577
                // Name item
578
                $def['All'][$class] = array(
579
                    'title' => $obj->i18n_singular_name()
580
                );
581
582
                // Check if can be created at the root
583
                $needsPerm = $obj->stat('need_permission');
584
                if (!$obj->stat('can_be_root')
585
                    || (!array_key_exists($class, $cacheCanCreate) || !$cacheCanCreate[$class])
586
                    || ($needsPerm && !$this->can($needsPerm))
587
                ) {
588
                    $def['Root']['disallowedChildren'][] = $class;
589
                }
590
591
                // Hint data specific to the class
592
                $def[$class] = array();
593
594
                $defaultChild = $obj->defaultChild();
595
                if ($defaultChild !== 'Page' && $defaultChild !== null) {
596
                    $def[$class]['defaultChild'] = $defaultChild;
597
                }
598
599
                $defaultParent = $obj->defaultParent();
600
                if ($defaultParent !== 1 && $defaultParent !== null) {
601
                    $def[$class]['defaultParent'] = $defaultParent;
602
                }
603
            }
604
605
            $this->extend('updateSiteTreeHints', $def);
606
607
            $json = Convert::raw2json($def);
608
            $cache->set($cacheKey, $json);
609
        }
610
        return $json;
611
    }
612
613
    /**
614
     * Populates an array of classes in the CMS
615
     * which allows the user to change the page type.
616
     *
617
     * @return SS_List
618
     */
619
    public function PageTypes()
620
    {
621
        $classes = SiteTree::page_type_classes();
622
623
        $result = new ArrayList();
624
625
        foreach ($classes as $class) {
626
            $instance = SiteTree::singleton($class);
627
            if ($instance instanceof HiddenClass) {
628
                continue;
629
            }
630
631
            // skip this type if it is restricted
632
            if ($instance->stat('need_permission') && !$this->can(singleton($class)->stat('need_permission'))) {
633
                continue;
634
            }
635
636
            $singularName = $instance->i18n_singular_name();
637
            $description = $instance->i18n_classDescription();
638
639
            $result->push(new ArrayData(array(
640
                'ClassName' => $class,
641
                'AddAction' => $singularName,
642
                'Description' => $description,
643
                // TODO Sprite support
644
                'IconURL' => $instance->stat('icon'),
645
                'Title' => $singularName,
646
            )));
647
        }
648
649
        $result = $result->sort('AddAction');
650
651
        return $result;
652
    }
653
654
    /**
655
     * Get a database record to be managed by the CMS.
656
     *
657
     * @param int $id Record ID
658
     * @param int $versionID optional Version id of the given record
659
     * @return SiteTree
660
     */
661
    public function getRecord($id, $versionID = null)
662
    {
663
        $treeClass = $this->stat('tree_class');
664
665
        if ($id instanceof $treeClass) {
666
            return $id;
667
        } elseif ($id && is_numeric($id)) {
668
            $currentStage = Versioned::get_reading_mode();
669
670
            if ($this->getRequest()->getVar('Version')) {
671
                $versionID = (int) $this->getRequest()->getVar('Version');
672
            }
673
674
            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...
675
                $record = Versioned::get_version($treeClass, $id, $versionID);
676
            } else {
677
                $record = DataObject::get_by_id($treeClass, $id);
678
            }
679
680
            // Then, try getting a record from the live site
681
            if (!$record) {
682
                // $record = Versioned::get_one_by_stage($treeClass, "Live", "\"$treeClass\".\"ID\" = $id");
683
                Versioned::set_stage(Versioned::LIVE);
684
                singleton($treeClass)->flushCache();
685
686
                $record = DataObject::get_by_id($treeClass, $id);
687
            }
688
689
            // Then, try getting a deleted record
690
            if (!$record) {
691
                $record = Versioned::get_latest_version($treeClass, $id);
692
            }
693
694
            // Don't open a page from a different locale
695
            /** The record's Locale is saved in database in 2.4, and not related with Session,
696
             *  we should not check their locale matches the Translatable::get_current_locale,
697
             *  here as long as we all the HTTPRequest is init with right locale.
698
             *  This bit breaks the all FileIFrameField functions if the field is used in CMS
699
             *  and its relevent ajax calles, like loading the tree dropdown for TreeSelectorField.
700
             */
701
            /* if($record && SiteTree::has_extension('Translatable') && $record->Locale && $record->Locale != Translatable::get_current_locale()) {
702
				$record = null;
703
			}*/
704
705
            // Set the reading mode back to what it was.
706
            Versioned::set_reading_mode($currentStage);
707
708
            return $record;
709
        } elseif (substr($id, 0, 3) == 'new') {
710
            return $this->getNewItem($id);
711
        }
712
    }
713
714
    /**
715
     * @param int $id
716
     * @param FieldList $fields
717
     * @return Form
718
     */
719
    public function getEditForm($id = null, $fields = null)
720
    {
721
        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...
722
            $id = $this->currentPageID();
723
        }
724
        $form = parent::getEditForm($id, $fields);
725
726
        // TODO Duplicate record fetching (see parent implementation)
727
        $record = $this->getRecord($id);
728
        if ($record && !$record->canView()) {
729
            return Security::permissionFailure($this);
0 ignored issues
show
Bug Compatibility introduced by
The expression \SilverStripe\Security\S...rmissionFailure($this); of type SilverStripe\Control\HTTPResponse|null adds the type SilverStripe\Control\HTTPResponse to the return on line 729 which is incompatible with the return type documented by SilverStripe\CMS\Controllers\CMSMain::getEditForm of type SilverStripe\Forms\Form.
Loading history...
730
        }
731
732
        if (!$fields) {
733
            $fields = $form->Fields();
734
        }
735
        $actions = $form->Actions();
0 ignored issues
show
Unused Code introduced by
$actions is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
736
737
        if ($record) {
738
            $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...
739
740
            $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...
741
            // Necessary for different subsites
742
            $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...
743
            $fields->push($liveLinkField = new HiddenField("LiveLink"));
744
            $fields->push($stageLinkField = new HiddenField("StageLink"));
745
            $fields->push(new HiddenField("TreeTitle", false, $record->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...
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...
746
747
            if ($record->ID && is_numeric($record->ID)) {
748
                $liveLink = $record->getAbsoluteLiveLink();
749
                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...
750
                    $liveLinkField->setValue($liveLink);
751
                }
752
                if (!$deletedFromStage) {
753
                    $stageLink = Controller::join_links($record->AbsoluteLink(), '?stage=Stage');
754
                    if ($stageLink) {
755
                        $stageLinkField->setValue($stageLink);
756
                    }
757
                }
758
            }
759
760
            // Added in-line to the form, but plucked into different view by LeftAndMain.Preview.js upon load
761
            /** @skipUpgrade */
762
            if ($record instanceof CMSPreviewable && !$fields->fieldByName('SilverStripeNavigator')) {
763
                $navField = new LiteralField('SilverStripeNavigator', $this->getSilverStripeNavigator());
764
                $navField->setAllowHTML(true);
765
                $fields->push($navField);
766
            }
767
768
            // getAllCMSActions can be used to completely redefine the action list
769
            if ($record->hasMethod('getAllCMSActions')) {
770
                $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...
771
            } else {
772
                $actions = $record->getCMSActions();
773
774
                // Find and remove action menus that have no actions.
775
                if ($actions && $actions->count()) {
776
                    /** @var TabSet $tabset */
777
                    $tabset = $actions->fieldByName('ActionMenus');
778
                    if ($tabset) {
779
                        foreach ($tabset->getChildren() as $tab) {
780
                            if (!$tab->getChildren()->count()) {
781
                                $tabset->removeByName($tab->getName());
782
                            }
783
                        }
784
                    }
785
                }
786
            }
787
788
            // Use <button> to allow full jQuery UI styling
789
            $actionsFlattened = $actions->dataFields();
790
            if ($actionsFlattened) {
791
                /** @var FormAction $action */
792
                foreach ($actionsFlattened as $action) {
793
                    $action->setUseButtonTag(true);
794
                }
795
            }
796
797
            if ($record->hasMethod('getCMSValidator')) {
798
                $validator = $record->getCMSValidator();
0 ignored issues
show
Documentation Bug introduced by
The method getCMSValidator 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...
Unused Code introduced by
$validator is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
799
            } else {
800
                $validator = new RequiredFields();
0 ignored issues
show
Unused Code introduced by
$validator is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
801
            }
802
803
            // TODO Can't merge $FormAttributes in template at the moment
804
            $form->addExtraClass('center ' . $this->BaseCSSClasses());
805
            // Set validation exemptions for specific actions
806
            $form->setValidationExemptActions(array('restore', 'revert', 'deletefromlive', 'delete', 'unpublish', 'rollback', 'doRollback'));
807
808
            // Announce the capability so the frontend can decide whether to allow preview or not.
809
            if ($record instanceof CMSPreviewable) {
810
                $form->addExtraClass('cms-previewable');
811
            }
812
            $form->addExtraClass('fill-height flexbox-area-grow');
813
814
            if (!$record->canEdit() || $deletedFromStage) {
815
                $readonlyFields = $form->Fields()->makeReadonly();
816
                $form->setFields($readonlyFields);
817
            }
818
819
            $form->Fields()->setForm($form);
820
821
            $this->extend('updateEditForm', $form);
822
            return $form;
823
        } elseif ($id) {
824
            $form = Form::create($this, "EditForm", new FieldList(
825
                new LabelField('PageDoesntExistLabel', _t('CMSMain.PAGENOTEXISTS', "This page doesn't exist"))
826
            ), new FieldList())->setHTMLID('Form_EditForm');
827
            return $form;
828
        }
829
    }
830
831
    /**
832
     * @param HTTPRequest $request
833
     * @return string HTML
834
     */
835
    public function treeview($request)
836
    {
837
        return $this->getResponseNegotiator()->respond($request);
838
    }
839
840
    /**
841
     * @param HTTPRequest $request
842
     * @return string HTML
843
     */
844
    public function listview($request)
845
    {
846
        return $this->getResponseNegotiator()->respond($request);
847
    }
848
849
    /**
850
     * @return string
851
     */
852
    public function ViewState()
853
    {
854
        $mode = $this->getRequest()->requestVar('view')
855
            ?: $this->getRequest()->param('Action');
856
        switch ($mode) {
857
            case 'listview':
858
            case 'treeview':
859
                return $mode;
860
            default:
861
                return 'treeview';
862
        }
863
    }
864
865
    /**
866
     * Callback to request the list of page types allowed under a given page instance.
867
     * Provides a slower but more precise response over SiteTreeHints
868
     *
869
     * @param HTTPRequest $request
870
     * @return HTTPResponse
871
     */
872
    public function childfilter($request)
873
    {
874
        // Check valid parent specified
875
        $parentID = $request->requestVar('ParentID');
876
        $parent = SiteTree::get()->byID($parentID);
877
        if (!$parent || !$parent->exists()) {
878
            return $this->httpError(404);
879
        }
880
881
        // Build hints specific to this class
882
        // Identify disallows and set globals
883
        $classes = SiteTree::page_type_classes();
884
        $disallowedChildren = array();
885
        foreach ($classes as $class) {
886
            $obj = singleton($class);
887
            if ($obj instanceof HiddenClass) {
888
                continue;
889
            }
890
891
            if (!$obj->canCreate(null, array('Parent' => $parent))) {
892
                $disallowedChildren[] = $class;
893
            }
894
        }
895
896
        $this->extend('updateChildFilter', $disallowedChildren, $parentID);
897
        return $this
898
            ->getResponse()
899
            ->addHeader('Content-Type', 'application/json; charset=utf-8')
900
            ->setBody(Convert::raw2json($disallowedChildren));
901
    }
902
903
    /**
904
     * Safely reconstruct a selected filter from a given set of query parameters
905
     *
906
     * @param array $params Query parameters to use
907
     * @return CMSSiteTreeFilter The filter class, or null if none present
908
     * @throws InvalidArgumentException if invalid filter class is passed.
909
     */
910
    protected function getQueryFilter($params)
911
    {
912
        if (empty($params['FilterClass'])) {
913
            return null;
914
        }
915
        $filterClass = $params['FilterClass'];
916
        if (!is_subclass_of($filterClass, 'SilverStripe\\CMS\\Controllers\\CMSSiteTreeFilter')) {
917
            throw new InvalidArgumentException("Invalid filter class passed: {$filterClass}");
918
        }
919
        return $filterClass::create($params);
920
    }
921
922
    /**
923
     * Returns the pages meet a certain criteria as {@see CMSSiteTreeFilter} or the subpages of a parent page
924
     * defaulting to no filter and show all pages in first level.
925
     * Doubles as search results, if any search parameters are set through {@link SearchForm()}.
926
     *
927
     * @param array $params Search filter criteria
928
     * @param int $parentID Optional parent node to filter on (can't be combined with other search criteria)
929
     * @return SS_List
930
     * @throws InvalidArgumentException if invalid filter class is passed.
931
     */
932
    public function getList($params = array(), $parentID = 0)
933
    {
934
        if ($filter = $this->getQueryFilter($params)) {
935
            return $filter->getFilteredPages();
936
        } else {
937
            $list = DataList::create($this->stat('tree_class'));
938
            $parentID = is_numeric($parentID) ? $parentID : 0;
939
            return $list->filter("ParentID", $parentID);
940
        }
941
    }
942
943
    /**
944
     * @return Form
945
     */
946
    public function ListViewForm()
947
    {
948
        $params = $this->getRequest()->requestVar('q');
949
        $list = $this->getList($params, $parentID = $this->getRequest()->requestVar('ParentID'));
950
        $gridFieldConfig = GridFieldConfig::create()->addComponents(
951
            new GridFieldSortableHeader(),
952
            new GridFieldDataColumns(),
953
            new GridFieldPaginator($this->config()->get('page_length'))
954
        );
955
        if ($parentID) {
956
            $linkSpec = $this->Link();
957
            $linkSpec = $linkSpec . (strstr($linkSpec, '?') ? '&' : '?') . 'ParentID=%d&view=listview';
958
            $gridFieldConfig->addComponent(
959
                GridFieldLevelup::create($parentID)
960
                    ->setLinkSpec($linkSpec)
961
                    ->setAttributes(array('data-pjax' => 'ListViewForm,Breadcrumbs'))
962
            );
963
        }
964
        $gridField = new GridField('Page', 'Pages', $list, $gridFieldConfig);
965
        /** @var GridFieldDataColumns $columns */
966
        $columns = $gridField->getConfig()->getComponentByType('SilverStripe\\Forms\\GridField\\GridFieldDataColumns');
967
968
        // Don't allow navigating into children nodes on filtered lists
969
        $fields = array(
970
            'getTreeTitle' => _t('SiteTree.PAGETITLE', 'Page Title'),
971
            'singular_name' => _t('SiteTree.PAGETYPE', 'Page Type'),
972
            'LastEdited' => _t('SiteTree.LASTUPDATED', 'Last Updated'),
973
        );
974
        /** @var GridFieldSortableHeader $sortableHeader */
975
        $sortableHeader = $gridField->getConfig()->getComponentByType('SilverStripe\\Forms\\GridField\\GridFieldSortableHeader');
976
        $sortableHeader->setFieldSorting(array('getTreeTitle' => 'Title'));
977
        $gridField->getState()->ParentID = $parentID;
978
979
        if (!$params) {
980
            $fields = array_merge(array('listChildrenLink' => ''), $fields);
981
        }
982
983
        $columns->setDisplayFields($fields);
984
        $columns->setFieldCasting(array(
985
            'Created' => 'DBDatetime->Ago',
986
            'LastEdited' => 'DBDatetime->FormatFromSettings',
987
            'getTreeTitle' => 'HTMLFragment'
988
        ));
989
990
        $controller = $this;
991
        $columns->setFieldFormatting(array(
992
            'listChildrenLink' => function ($value, &$item) use ($controller) {
993
                /** @var SiteTree $item */
994
                $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...
995
                if ($num) {
996
                    return sprintf(
997
                        '<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>',
998
                        Controller::join_links(
999
                            $controller->Link(),
1000
                            sprintf("?ParentID=%d&view=listview", (int)$item->ID)
1001
                        ),
1002
                        $num
1003
                    );
1004
                }
1005
            },
1006
            'getTreeTitle' => function ($value, &$item) use ($controller) {
1007
                return sprintf(
1008
                    '<a class="action-detail" href="%s">%s</a>',
1009
                    Controller::join_links(
1010
                        CMSPageEditController::singleton()->Link('show'),
1011
                        (int)$item->ID
1012
                    ),
1013
                    $item->TreeTitle // returns HTML, does its own escaping
1014
                );
1015
            }
1016
        ));
1017
1018
        $negotiator = $this->getResponseNegotiator();
1019
        $listview = Form::create(
1020
            $this,
1021
            'ListViewForm',
1022
            new FieldList($gridField),
1023
            new FieldList()
1024
        )->setHTMLID('Form_ListViewForm');
1025
        $listview->setAttribute('data-pjax-fragment', 'ListViewForm');
1026 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...
1027
            $request = $this->getRequest();
1028
            if ($request->isAjax() && $negotiator) {
1029
                $result = $listview->forTemplate();
1030
                return $negotiator->respond($request, array(
1031
                    'CurrentForm' => function () use ($result) {
1032
                        return $result;
1033
                    }
1034
                ));
1035
            }
1036
        });
1037
1038
        $this->extend('updateListView', $listview);
1039
1040
        $listview->disableSecurityToken();
1041
        return $listview;
1042
    }
1043
1044
    public function currentPageID()
1045
    {
1046
        $id = parent::currentPageID();
1047
1048
        $this->extend('updateCurrentPageID', $id);
1049
1050
        return $id;
1051
    }
1052
1053
    //------------------------------------------------------------------------------------------//
1054
    // Data saving handlers
1055
1056
    /**
1057
     * Save and Publish page handler
1058
     *
1059
     * @param array $data
1060
     * @param Form $form
1061
     * @return HTTPResponse
1062
     * @throws HTTPResponse_Exception
1063
     */
1064
    public function save($data, $form)
1065
    {
1066
        $className = $this->stat('tree_class');
1067
1068
        // Existing or new record?
1069
        $id = $data['ID'];
1070
        if (substr($id, 0, 3) != 'new') {
1071
            /** @var SiteTree $record */
1072
            $record = DataObject::get_by_id($className, $id);
1073
            // Check edit permissions
1074
            if ($record && !$record->canEdit()) {
1075
                return Security::permissionFailure($this);
1076
            }
1077
            if (!$record || !$record->ID) {
1078
                throw new HTTPResponse_Exception("Bad record ID #$id", 404);
1079
            }
1080
        } else {
1081
            if (!$className::singleton()->canCreate()) {
1082
                return Security::permissionFailure($this);
1083
            }
1084
            $record = $this->getNewItem($id, false);
1085
        }
1086
1087
        // Check publishing permissions
1088
        $doPublish = !empty($data['publish']);
1089
        if ($record && $doPublish && !$record->canPublish()) {
1090
            return Security::permissionFailure($this);
1091
        }
1092
1093
        // TODO Coupling to SiteTree
1094
        $record->HasBrokenLink = 0;
1095
        $record->HasBrokenFile = 0;
1096
1097
        if (!$record->ObsoleteClassName) {
1098
            $record->writeWithoutVersion();
1099
        }
1100
1101
        // Update the class instance if necessary
1102
        if (isset($data['ClassName']) && $data['ClassName'] != $record->ClassName) {
1103
            // Replace $record with a new instance of the new class
1104
            $newClassName = $data['ClassName'];
1105
            $record = $record->newClassInstance($newClassName);
1106
        }
1107
1108
        // save form data into record
1109
        $form->saveInto($record);
1110
        $record->write();
1111
1112
        // If the 'Save & Publish' button was clicked, also publish the page
1113
        if ($doPublish) {
1114
            $record->publishRecursive();
1115
            $message = _t(
1116
                'CMSMain.PUBLISHED',
1117
                "Published '{title}' successfully.",
1118
                ['title' => $record->Title]
1119
            );
1120
        } else {
1121
            $message = _t(
1122
                'CMSMain.SAVED',
1123
                "Saved '{title}' successfully.",
1124
                ['title' => $record->Title]
1125
            );
1126
        }
1127
1128
        $this->getResponse()->addHeader('X-Status', rawurlencode($message));
1129
        return $this->getResponseNegotiator()->respond($this->getRequest());
1130
    }
1131
1132
    /**
1133
     * @uses LeftAndMainExtension->augmentNewSiteTreeItem()
1134
     *
1135
     * @param int|string $id
1136
     * @param bool $setID
1137
     * @return mixed|DataObject
1138
     * @throws HTTPResponse_Exception
1139
     */
1140
    public function getNewItem($id, $setID = true)
1141
    {
1142
        $parentClass = $this->stat('tree_class');
1143
        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...
1144
1145
        if (!is_a($className, $parentClass, true)) {
1146
            $response = Security::permissionFailure($this);
1147
            if (!$response) {
1148
                $response = $this->getResponse();
1149
            }
1150
            throw new HTTPResponse_Exception($response);
1151
        }
1152
1153
        /** @var SiteTree $newItem */
1154
        $newItem = Injector::inst()->create($className);
1155
        if (!$suffix) {
1156
            $sessionTag = "NewItems." . $parentID . "." . $className;
1157
            if (Session::get($sessionTag)) {
1158
                $suffix = '-' . Session::get($sessionTag);
1159
                Session::set($sessionTag, Session::get($sessionTag) + 1);
1160
            } else {
1161
                Session::set($sessionTag, 1);
1162
            }
1163
1164
                $id = $id . $suffix;
1165
        }
1166
1167
        $newItem->Title = _t(
1168
            'CMSMain.NEWPAGE',
1169
            "New {pagetype}",
1170
            'followed by a page type title',
1171
            array('pagetype' => singleton($className)->i18n_singular_name())
1172
        );
1173
        $newItem->ClassName = $className;
1174
        $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...
1175
1176
        // DataObject::fieldExists only checks the current class, not the hierarchy
1177
        // This allows the CMS to set the correct sort value
1178
        if ($newItem->castingHelper('Sort')) {
1179
            $newItem->Sort = DB::prepared_query('SELECT MAX("Sort") FROM "SiteTree" WHERE "ParentID" = ?', array($parentID))->value() + 1;
1180
        }
1181
1182
        if ($setID) {
1183
            $newItem->ID = $id;
1184
        }
1185
1186
        # Some modules like subsites add extra fields that need to be set when the new item is created
1187
        $this->extend('augmentNewSiteTreeItem', $newItem);
1188
1189
        return $newItem;
1190
    }
1191
1192
    /**
1193
     * Actually perform the publication step
1194
     *
1195
     * @param Versioned|DataObject $record
1196
     * @return mixed
1197
     */
1198
    public function performPublish($record)
1199
    {
1200
        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...
1201
            return Security::permissionFailure($this);
1202
        }
1203
1204
        $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...
1205
    }
1206
1207
    /**
1208
     * Reverts a page by publishing it to live.
1209
     * Use {@link restorepage()} if you want to restore a page
1210
     * which was deleted from draft without publishing.
1211
     *
1212
     * @uses SiteTree->doRevertToLive()
1213
     *
1214
     * @param array $data
1215
     * @param Form $form
1216
     * @return HTTPResponse
1217
     * @throws HTTPResponse_Exception
1218
     */
1219
    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...
1220
    {
1221
        if (!isset($data['ID'])) {
1222
            throw new HTTPResponse_Exception("Please pass an ID in the form content", 400);
1223
        }
1224
1225
        $id = (int) $data['ID'];
1226
        $restoredPage = Versioned::get_latest_version(SiteTree::class, $id);
1227
        if (!$restoredPage) {
1228
            throw new HTTPResponse_Exception("SiteTree #$id not found", 400);
1229
        }
1230
1231
        /** @var SiteTree $record */
1232
        $record = Versioned::get_one_by_stage('SilverStripe\\CMS\\Model\\SiteTree', 'Live', array(
1233
            '"SiteTree_Live"."ID"' => $id
1234
        ));
1235
1236
        // a user can restore a page without publication rights, as it just adds a new draft state
1237
        // (this action should just be available when page has been "deleted from draft")
1238
        if ($record && !$record->canEdit()) {
1239
            return Security::permissionFailure($this);
1240
        }
1241
        if (!$record || !$record->ID) {
1242
            throw new HTTPResponse_Exception("Bad record ID #$id", 404);
1243
        }
1244
1245
        $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...
1246
1247
        $this->getResponse()->addHeader(
1248
            'X-Status',
1249
            rawurlencode(_t(
1250
                'CMSMain.RESTORED',
1251
                "Restored '{title}' successfully",
1252
                'Param %s is a title',
1253
                array('title' => $record->Title)
1254
            ))
1255
        );
1256
1257
        return $this->getResponseNegotiator()->respond($this->getRequest());
1258
    }
1259
1260
    /**
1261
     * Delete the current page from draft stage.
1262
     *
1263
     * @see deletefromlive()
1264
     *
1265
     * @param array $data
1266
     * @param Form $form
1267
     * @return HTTPResponse
1268
     * @throws HTTPResponse_Exception
1269
     */
1270 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...
1271
    {
1272
        $id = $data['ID'];
1273
        $record = SiteTree::get()->byID($id);
1274
        if ($record && !$record->canDelete()) {
1275
            return Security::permissionFailure();
1276
        }
1277
        if (!$record || !$record->ID) {
1278
            throw new HTTPResponse_Exception("Bad record ID #$id", 404);
1279
        }
1280
1281
        // Delete record
1282
        $record->delete();
1283
1284
        $this->getResponse()->addHeader(
1285
            'X-Status',
1286
            rawurlencode(sprintf(_t('CMSMain.REMOVEDPAGEFROMDRAFT', "Removed '%s' from the draft site"), $record->Title))
1287
        );
1288
1289
        // Even if the record has been deleted from stage and live, it can be viewed in "archive mode"
1290
        return $this->getResponseNegotiator()->respond($this->getRequest());
1291
    }
1292
1293
    /**
1294
     * Delete this page from both live and stage
1295
     *
1296
     * @param array $data
1297
     * @param Form $form
1298
     * @return HTTPResponse
1299
     * @throws HTTPResponse_Exception
1300
     */
1301 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...
1302
    {
1303
        $id = $data['ID'];
1304
        /** @var SiteTree $record */
1305
        $record = SiteTree::get()->byID($id);
1306
        if (!$record || !$record->exists()) {
1307
            throw new HTTPResponse_Exception("Bad record ID #$id", 404);
1308
        }
1309
        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...
1310
            return Security::permissionFailure();
1311
        }
1312
1313
        // Archive record
1314
        $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...
1315
1316
        $this->getResponse()->addHeader(
1317
            'X-Status',
1318
            rawurlencode(sprintf(_t('CMSMain.ARCHIVEDPAGE', "Archived page '%s'"), $record->Title))
1319
        );
1320
1321
        // Even if the record has been deleted from stage and live, it can be viewed in "archive mode"
1322
        return $this->getResponseNegotiator()->respond($this->getRequest());
1323
    }
1324
1325
    public function publish($data, $form)
1326
    {
1327
        $data['publish'] = '1';
1328
1329
        return $this->save($data, $form);
1330
    }
1331
1332
    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...
1333
    {
1334
        $className = $this->stat('tree_class');
1335
        /** @var SiteTree $record */
1336
        $record = DataObject::get_by_id($className, $data['ID']);
1337
1338
        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...
1339
            return Security::permissionFailure($this);
1340
        }
1341
        if (!$record || !$record->ID) {
1342
            throw new HTTPResponse_Exception("Bad record ID #" . (int)$data['ID'], 404);
1343
        }
1344
1345
        $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...
1346
1347
        $this->getResponse()->addHeader(
1348
            'X-Status',
1349
            rawurlencode(_t('CMSMain.REMOVEDPAGE', "Removed '{title}' from the published site", array('title' => $record->Title)))
1350
        );
1351
1352
        return $this->getResponseNegotiator()->respond($this->getRequest());
1353
    }
1354
1355
    /**
1356
     * @return HTTPResponse
1357
     */
1358
    public function rollback()
1359
    {
1360
        return $this->doRollback(array(
1361
            'ID' => $this->currentPageID(),
1362
            'Version' => $this->getRequest()->param('VersionID')
1363
        ), 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...
1364
    }
1365
1366
    /**
1367
     * Rolls a site back to a given version ID
1368
     *
1369
     * @param array $data
1370
     * @param Form $form
1371
     * @return HTTPResponse
1372
     */
1373
    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...
1374
    {
1375
        $this->extend('onBeforeRollback', $data['ID']);
1376
1377
        $id = (isset($data['ID'])) ? (int) $data['ID'] : null;
1378
        $version = (isset($data['Version'])) ? (int) $data['Version'] : null;
1379
1380
        /** @var DataObject|Versioned $record */
1381
        $record = DataObject::get_by_id($this->stat('tree_class'), $id);
1382
        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...
1383
            return Security::permissionFailure($this);
1384
        }
1385
1386
        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...
1387
            $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...
1388
            $message = _t(
1389
                'CMSMain.ROLLEDBACKVERSIONv2',
1390
                "Rolled back to version #%d.",
1391
                array('version' => $data['Version'])
1392
            );
1393
        } else {
1394
            $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...
1395
            $message = _t(
1396
                'CMSMain.ROLLEDBACKPUBv2',
1397
                "Rolled back to published version."
1398
            );
1399
        }
1400
1401
        $this->getResponse()->addHeader('X-Status', rawurlencode($message));
1402
1403
        // Can be used in different contexts: In normal page edit view, in which case the redirect won't have any effect.
1404
        // Or in history view, in which case a revert causes the CMS to re-load the edit view.
1405
        // The X-Pjax header forces a "full" content refresh on redirect.
1406
        $url = Controller::join_links(CMSPageEditController::singleton()->Link('show'), $record->ID);
1407
        $this->getResponse()->addHeader('X-ControllerURL', $url);
1408
        $this->getRequest()->addHeader('X-Pjax', 'Content');
1409
        $this->getResponse()->addHeader('X-Pjax', 'Content');
1410
1411
        return $this->getResponseNegotiator()->respond($this->getRequest());
1412
    }
1413
1414
    /**
1415
     * Batch Actions Handler
1416
     */
1417
    public function batchactions()
1418
    {
1419
        return new CMSBatchActionHandler($this, 'batchactions');
1420
    }
1421
1422
    public function BatchActionParameters()
1423
    {
1424
        $batchActions = CMSBatchActionHandler::config()->batch_actions;
1425
1426
        $forms = array();
1427
        foreach ($batchActions as $urlSegment => $batchAction) {
1428
            $SNG_action = singleton($batchAction);
1429
            if ($SNG_action->canView() && $fieldset = $SNG_action->getParameterFields()) {
1430
                $formHtml = '';
1431
                /** @var FormField $field */
1432
                foreach ($fieldset as $field) {
1433
                    $formHtml .= $field->Field();
1434
                }
1435
                $forms[$urlSegment] = $formHtml;
1436
            }
1437
        }
1438
        $pageHtml = '';
1439
        foreach ($forms as $urlSegment => $html) {
1440
            $pageHtml .= "<div class=\"params\" id=\"BatchActionParameters_$urlSegment\">$html</div>\n\n";
1441
        }
1442
        return new LiteralField("BatchActionParameters", '<div id="BatchActionParameters" style="display:none">'.$pageHtml.'</div>');
1443
    }
1444
    /**
1445
     * Returns a list of batch actions
1446
     */
1447
    public function BatchActionList()
1448
    {
1449
        return $this->batchactions()->batchActionList();
1450
    }
1451
1452
    public function publishall($request)
1453
    {
1454
        if (!Permission::check('ADMIN')) {
1455
            return Security::permissionFailure($this);
1456
        }
1457
1458
        increase_time_limit_to();
1459
        increase_memory_limit_to();
1460
1461
        $response = "";
1462
1463
        if (isset($this->requestParams['confirm'])) {
1464
            // Protect against CSRF on destructive action
1465
            if (!SecurityToken::inst()->checkRequest($request)) {
1466
                return $this->httpError(400);
1467
            }
1468
1469
            $start = 0;
1470
            $pages = SiteTree::get()->limit("$start,30");
1471
            $count = 0;
1472
            while ($pages) {
1473
                /** @var SiteTree $page */
1474
                foreach ($pages as $page) {
1475
                    if ($page && !$page->canPublish()) {
1476
                        return Security::permissionFailure($this);
1477
                    }
1478
1479
                    $page->publishRecursive();
1480
                    $page->destroy();
1481
                    unset($page);
1482
                    $count++;
1483
                    $response .= "<li>$count</li>";
1484
                }
1485
                if ($pages->count() > 29) {
1486
                    $start += 30;
1487
                    $pages = SiteTree::get()->limit("$start,30");
1488
                } else {
1489
                    break;
1490
                }
1491
            }
1492
            $response .= _t('CMSMain.PUBPAGES', "Done: Published {count} pages", array('count' => $count));
1493
        } else {
1494
            $token = SecurityToken::inst();
1495
            $fields = new FieldList();
1496
            $token->updateFieldSet($fields);
1497
            $tokenField = $fields->first();
1498
            $tokenHtml = ($tokenField) ? $tokenField->FieldHolder() : '';
1499
            $publishAllDescription = _t(
1500
                'CMSMain.PUBALLFUN2',
1501
                'Pressing this button will do the equivalent of going to every page and pressing "publish".  '
1502
                . 'It\'s intended to be used after there have been massive edits of the content, such as when '
1503
                . 'the site was first built.'
1504
            );
1505
            $response .= '<h1>' . _t('CMSMain.PUBALLFUN', '"Publish All" functionality') . '</h1>
1506
				<p>' . $publishAllDescription . '</p>
1507
				<form method="post" action="publishall">
1508
					<input type="submit" name="confirm" value="'
1509
                    . _t('CMSMain.PUBALLCONFIRM', "Please publish every page in the site, copying content stage to live", 'Confirmation button') .'" />'
1510
                    . $tokenHtml .
1511
                '</form>';
1512
        }
1513
1514
        return $response;
1515
    }
1516
1517
    /**
1518
     * Restore a completely deleted page from the SiteTree_versions table.
1519
     *
1520
     * @param array $data
1521
     * @param Form $form
1522
     * @return HTTPResponse
1523
     */
1524
    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...
1525
    {
1526
        if (!isset($data['ID']) || !is_numeric($data['ID'])) {
1527
            return new HTTPResponse("Please pass an ID in the form content", 400);
1528
        }
1529
1530
        $id = (int)$data['ID'];
1531
        /** @var SiteTree $restoredPage */
1532
        $restoredPage = Versioned::get_latest_version(SiteTree::class, $id);
1533
        if (!$restoredPage) {
1534
            return new HTTPResponse("SiteTree #$id not found", 400);
1535
        }
1536
1537
        $restoredPage = $restoredPage->doRestoreToStage();
1538
1539
        $this->getResponse()->addHeader(
1540
            'X-Status',
1541
            rawurlencode(_t(
1542
                'CMSMain.RESTORED',
1543
                "Restored '{title}' successfully",
1544
                array('title' => $restoredPage->Title)
1545
            ))
1546
        );
1547
1548
        return $this->getResponseNegotiator()->respond($this->getRequest());
1549
    }
1550
1551
    public function duplicate($request)
1552
    {
1553
        // Protect against CSRF on destructive action
1554
        if (!SecurityToken::inst()->checkRequest($request)) {
1555
            return $this->httpError(400);
1556
        }
1557
1558
        if (($id = $this->urlParams['ID']) && is_numeric($id)) {
1559
            /** @var SiteTree $page */
1560
            $page = SiteTree::get()->byID($id);
1561 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...
1562
                return Security::permissionFailure($this);
1563
            }
1564
            if (!$page || !$page->ID) {
1565
                throw new HTTPResponse_Exception("Bad record ID #$id", 404);
1566
            }
1567
1568
            $newPage = $page->duplicate();
1569
1570
            // ParentID can be hard-set in the URL.  This is useful for pages with multiple parents
1571
            if (isset($_GET['parentID']) && is_numeric($_GET['parentID'])) {
1572
                $newPage->ParentID = $_GET['parentID'];
1573
                $newPage->write();
1574
            }
1575
1576
            $this->getResponse()->addHeader(
1577
                'X-Status',
1578
                rawurlencode(_t(
1579
                    'CMSMain.DUPLICATED',
1580
                    "Duplicated '{title}' successfully",
1581
                    array('title' => $newPage->Title)
1582
                ))
1583
            );
1584
            $url = Controller::join_links(CMSPageEditController::singleton()->Link('show'), $newPage->ID);
1585
            $this->getResponse()->addHeader('X-ControllerURL', $url);
1586
            $this->getRequest()->addHeader('X-Pjax', 'Content');
1587
            $this->getResponse()->addHeader('X-Pjax', 'Content');
1588
1589
            return $this->getResponseNegotiator()->respond($this->getRequest());
1590
        } else {
1591
            return new HTTPResponse("CMSMain::duplicate() Bad ID: '$id'", 400);
1592
        }
1593
    }
1594
1595
    public function duplicatewithchildren($request)
1596
    {
1597
        // Protect against CSRF on destructive action
1598
        if (!SecurityToken::inst()->checkRequest($request)) {
1599
            return $this->httpError(400);
1600
        }
1601
        increase_time_limit_to();
1602
        if (($id = $this->urlParams['ID']) && is_numeric($id)) {
1603
            /** @var SiteTree $page */
1604
            $page = SiteTree::get()->byID($id);
1605 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...
1606
                return Security::permissionFailure($this);
1607
            }
1608
            if (!$page || !$page->ID) {
1609
                throw new HTTPResponse_Exception("Bad record ID #$id", 404);
1610
            }
1611
1612
            $newPage = $page->duplicateWithChildren();
1613
1614
            $this->getResponse()->addHeader(
1615
                'X-Status',
1616
                rawurlencode(_t(
1617
                    'CMSMain.DUPLICATEDWITHCHILDREN',
1618
                    "Duplicated '{title}' and children successfully",
1619
                    array('title' => $newPage->Title)
1620
                ))
1621
            );
1622
            $url = Controller::join_links(CMSPageEditController::singleton()->Link('show'), $newPage->ID);
1623
            $this->getResponse()->addHeader('X-ControllerURL', $url);
1624
            $this->getRequest()->addHeader('X-Pjax', 'Content');
1625
            $this->getResponse()->addHeader('X-Pjax', 'Content');
1626
1627
            return $this->getResponseNegotiator()->respond($this->getRequest());
1628
        } else {
1629
            return new HTTPResponse("CMSMain::duplicatewithchildren() Bad ID: '$id'", 400);
1630
        }
1631
    }
1632
1633
    public function providePermissions()
1634
    {
1635
        $title = CMSPagesController::menu_title();
1636
        return array(
1637
            "CMS_ACCESS_CMSMain" => array(
1638
                'name' => _t('CMSMain.ACCESS', "Access to '{title}' section", array('title' => $title)),
1639
                'category' => _t('Permission.CMS_ACCESS_CATEGORY', 'CMS Access'),
1640
                'help' => _t(
1641
                    'CMSMain.ACCESS_HELP',
1642
                    '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".'
1643
                ),
1644
                'sort' => -99 // below "CMS_ACCESS_LeftAndMain", but above everything else
1645
            )
1646
        );
1647
    }
1648
}
1649