Completed
Pull Request — master (#1727)
by Damian
24:34
created

CMSMain::PageTypes()   C

Complexity

Conditions 9
Paths 11

Size

Total Lines 46
Code Lines 24

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 46
rs 5.0942
cc 9
eloc 24
nc 11
nop 0
1
<?php
2
3
namespace SilverStripe\CMS\Controllers;
4
5
use SilverStripe\Admin\AdminRootController;
6
use SilverStripe\Admin\CMSBatchActionHandler;
7
use SilverStripe\Admin\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 SilverStripe\Core\Cache;
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\ORM\Versioning\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 Zend_Cache;
63
use InvalidArgumentException;
64
65
/**
66
 * The main "content" area of the CMS.
67
 *
68
 * This class creates a 2-frame layout - left-tree and right-form - to sit beneath the main
69
 * admin menu.
70
 *
71
 * @todo Create some base classes to contain the generic functionality that will be replicated.
72
 *
73
 * @mixin LeftAndMainPageIconsExtension
74
 */
75
class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionProvider
76
{
77
78
    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...
79
80
    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...
81
82
    // Maintain a lower priority than other administration sections
83
    // so that Director does not think they are actions of CMSMain
84
    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...
85
86
    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...
87
88
    private static $menu_icon_class = 'font-icon-sitemap';
89
90
    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...
91
92
    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...
93
94
    private static $subitem_class = Member::class;
95
96
    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...
97
98
    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...
99
100
    /**
101
     * Amount of results showing on a single page.
102
     *
103
     * @config
104
     * @var int
105
     */
106
    private static $page_length = 15;
107
108
    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...
109
        'archive',
110
        'deleteitems',
111
        'DeleteItemsForm',
112
        'dialog',
113
        'duplicate',
114
        'duplicatewithchildren',
115
        'publishall',
116
        'publishitems',
117
        'PublishItemsForm',
118
        'submit',
119
        'EditForm',
120
        'SearchForm',
121
        'SiteTreeAsUL',
122
        'getshowdeletedsubtree',
123
        'batchactions',
124
        'treeview',
125
        'listview',
126
        'ListViewForm',
127
        'childfilter',
128
    );
129
130
    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...
131
        'TreeIsFiltered' => 'Boolean',
132
        'AddForm' => 'HTMLFragment',
133
        'LinkPages' => 'Text',
134
        'Link' => 'Text',
135
        'ListViewForm' => 'HTMLFragment',
136
        'ExtraTreeTools' => 'HTMLFragment',
137
        'PageList' => 'HTMLFragment',
138
        'PageListSidebar' => 'HTMLFragment',
139
        'SiteTreeHints' => 'HTMLFragment',
140
        'SecurityID' => 'Text',
141
        'SiteTreeAsUL' => 'HTMLFragment',
142
    );
143
144
    protected function init()
145
    {
146
        // set reading lang
147
        if (SiteTree::has_extension('Translatable') && !$this->getRequest()->isAjax()) {
148
            Translatable::choose_site_locale(array_keys(Translatable::get_existing_content_languages('SilverStripe\\CMS\\Model\\SiteTree')));
149
        }
150
151
        parent::init();
152
153
        Requirements::javascript(CMS_DIR . '/client/dist/js/bundle.js');
154
        Requirements::javascript(CMS_DIR . '/client/dist/js/SilverStripeNavigator.js');
155
        Requirements::css(CMS_DIR . '/client/dist/styles/bundle.css');
156
        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...
157
        Requirements::add_i18n_javascript(CMS_DIR . '/client/lang', false, true);
158
159
        CMSBatchActionHandler::register('restore', CMSBatchAction_Restore::class);
160
        CMSBatchActionHandler::register('archive', CMSBatchAction_Archive::class);
161
        CMSBatchActionHandler::register('unpublish', CMSBatchAction_Unpublish::class);
162
        CMSBatchActionHandler::register('publish', CMSBatchAction_Publish::class);
163
    }
164
165
    public function index($request)
166
    {
167
        // In case we're not showing a specific record, explicitly remove any session state,
168
        // to avoid it being highlighted in the tree, and causing an edit form to show.
169
        if (!$request->param('Action')) {
170
            $this->setCurrentPageID(null);
171
        }
172
173
        return parent::index($request);
174
    }
175
176
    public function getResponseNegotiator()
177
    {
178
        $negotiator = parent::getResponseNegotiator();
179
180
        // ListViewForm
181
        $negotiator->setCallback('ListViewForm', function () {
182
            return $this->ListViewForm()->forTemplate();
183
        });
184
185
        // PageList view
186
        $negotiator->setCallback('Content-PageList', function () {
187
            return $this->PageList()->forTemplate();
188
        });
189
190
        // PageList view for edit controller
191
        $negotiator->setCallback('Content-PageList-Sidebar', function () {
192
            return $this->PageListSidebar()->forTemplate();
193
        });
194
195
        return $negotiator;
196
    }
197
198
    /**
199
     * Get pages listing area
200
     *
201
     * @return DBHTMLText
202
     */
203
    public function PageList()
204
    {
205
        return $this->renderWith($this->getTemplatesWithSuffix('_PageList'));
206
    }
207
208
    /**
209
     * Page list view for edit-form
210
     *
211
     * @return DBHTMLText
212
     */
213
    public function PageListSidebar()
214
    {
215
        return $this->renderWith($this->getTemplatesWithSuffix('_PageList_Sidebar'));
216
    }
217
218
    /**
219
     * If this is set to true, the "switchView" context in the
220
     * template is shown, with links to the staging and publish site.
221
     *
222
     * @return boolean
223
     */
224
    public function ShowSwitchView()
225
    {
226
        return true;
227
    }
228
229
    /**
230
     * Overloads the LeftAndMain::ShowView. Allows to pass a page as a parameter, so we are able
231
     * to switch view also for archived versions.
232
     *
233
     * @param SiteTree $page
234
     * @return array
235
     */
236
    public function SwitchView($page = null)
237
    {
238
        if (!$page) {
239
            $page = $this->currentPage();
240
        }
241
242
        if ($page) {
243
            $nav = SilverStripeNavigator::get_for_record($page);
244
            return $nav['items'];
245
        }
246
    }
247
248
    //------------------------------------------------------------------------------------------//
249
    // Main controllers
250
251
    //------------------------------------------------------------------------------------------//
252
    // Main UI components
253
254
    /**
255
     * Override {@link LeftAndMain} Link to allow blank URL segment for CMSMain.
256
     *
257
     * @param string|null $action Action to link to.
258
     * @return string
259
     */
260
    public function Link($action = null)
261
    {
262
        $link = Controller::join_links(
263
            AdminRootController::admin_url(),
264
            $this->stat('url_segment'), // in case we want to change the segment
265
            '/', // trailing slash needed if $action is null!
266
            "$action"
267
        );
268
        $this->extend('updateLink', $link);
269
        return $link;
270
    }
271
272
    public function LinkPages()
273
    {
274
        return CMSPagesController::singleton()->Link();
275
    }
276
277
    public function LinkPagesWithSearch()
278
    {
279
        return $this->LinkWithSearch($this->LinkPages());
280
    }
281
282
    /**
283
     * Get link to tree view
284
     *
285
     * @return string
286
     */
287
    public function LinkTreeView()
288
    {
289
        // Tree view is just default link to main pages section (no /treeview suffix)
290
        return $this->LinkWithSearch(CMSMain::singleton()->Link());
291
    }
292
293
    /**
294
     * Get link to list view
295
     *
296
     * @return string
297
     */
298
    public function LinkListView()
299
    {
300
        // Note : Force redirect to top level page controller
301
        return $this->LinkWithSearch(CMSMain::singleton()->Link('listview'));
302
    }
303
304 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...
305
    {
306
        if (!$id) {
307
            $id = $this->currentPageID();
308
        }
309
        return $this->LinkWithSearch(
310
            Controller::join_links(CMSPageEditController::singleton()->Link('show'), $id)
311
        );
312
    }
313
314 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...
315
    {
316
        if ($id = $this->currentPageID()) {
317
            return $this->LinkWithSearch(
318
                Controller::join_links(CMSPageSettingsController::singleton()->Link('show'), $id)
319
            );
320
        } else {
321
            return null;
322
        }
323
    }
324
325 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...
326
    {
327
        if ($id = $this->currentPageID()) {
328
            return $this->LinkWithSearch(
329
                Controller::join_links(CMSPageHistoryController::singleton()->Link('show'), $id)
330
            );
331
        } else {
332
            return null;
333
        }
334
    }
335
336
    public function LinkWithSearch($link)
337
    {
338
        // Whitelist to avoid side effects
339
        $params = array(
340
            'q' => (array)$this->getRequest()->getVar('q'),
341
            'ParentID' => $this->getRequest()->getVar('ParentID')
342
        );
343
        $link = Controller::join_links(
344
            $link,
345
            array_filter(array_values($params)) ? '?' . http_build_query($params) : null
346
        );
347
        $this->extend('updateLinkWithSearch', $link);
348
        return $link;
349
    }
350
351
    public function LinkPageAdd($extra = null, $placeholders = null)
352
    {
353
        $link = CMSPageAddController::singleton()->Link();
354
        $this->extend('updateLinkPageAdd', $link);
355
356
        if ($extra) {
357
            $link = Controller::join_links($link, $extra);
358
        }
359
360
        if ($placeholders) {
361
            $link .= (strpos($link, '?') === false ? "?$placeholders" : "&$placeholders");
362
        }
363
364
        return $link;
365
    }
366
367
    /**
368
     * @return string
369
     */
370
    public function LinkPreview()
371
    {
372
        $record = $this->getRecord($this->currentPageID());
373
        $baseLink = Director::absoluteBaseURL();
374
        if ($record && $record instanceof SiteTree) {
375
            // if we are an external redirector don't show a link
376
            if ($record instanceof RedirectorPage && $record->RedirectionType == 'External') {
377
                $baseLink = false;
378
            } else {
379
                $baseLink = $record->Link('?stage=Stage');
380
            }
381
        }
382
        return $baseLink;
383
    }
384
385
    /**
386
     * Return the entire site tree as a nested set of ULs
387
     */
388
    public function SiteTreeAsUL()
389
    {
390
        // Pre-cache sitetree version numbers for querying efficiency
391
        Versioned::prepopulate_versionnumber_cache(SiteTree::class, "Stage");
392
        Versioned::prepopulate_versionnumber_cache(SiteTree::class, "Live");
393
        $html = $this->getSiteTreeFor($this->stat('tree_class'));
394
395
        $this->extend('updateSiteTreeAsUL', $html);
396
397
        return $html;
398
    }
399
400
    /**
401
     * @return boolean
402
     */
403
    public function TreeIsFiltered()
404
    {
405
        $query = $this->getRequest()->getVar('q');
406
407
        if (!$query || (count($query) === 1 && isset($query['FilterClass']) && $query['FilterClass'] === 'SilverStripe\\CMS\\Controllers\\CMSSiteTreeFilter_Search')) {
408
            return false;
409
        }
410
411
        return true;
412
    }
413
414
    public function ExtraTreeTools()
415
    {
416
        $html = '';
417
        $this->extend('updateExtraTreeTools', $html);
418
        return $html;
419
    }
420
421
    /**
422
     * Returns a Form for page searching for use in templates.
423
     *
424
     * Can be modified from a decorator by a 'updateSearchForm' method
425
     *
426
     * @return Form
427
     */
428
    public function SearchForm()
429
    {
430
        // Create the fields
431
        $content = new TextField('q[Term]', _t('CMSSearch.FILTERLABELTEXT', 'Search'));
432
        $dateFrom = new DateField(
433
            'q[LastEditedFrom]',
434
            _t('CMSSearch.FILTERDATEFROM', 'From')
435
        );
436
        $dateFrom->setConfig('showcalendar', true);
437
        $dateTo = new DateField(
438
            'q[LastEditedTo]',
439
            _t('CMSSearch.FILTERDATETO', 'To')
440
        );
441
        $dateTo->setConfig('showcalendar', true);
442
        $pageFilter = new DropdownField(
443
            'q[FilterClass]',
444
            _t('CMSMain.PAGES', 'Page status'),
445
            CMSSiteTreeFilter::get_all_filters()
446
        );
447
        $pageClasses = new DropdownField(
448
            'q[ClassName]',
449
            _t('CMSMain.PAGETYPEOPT', 'Page type', 'Dropdown for limiting search to a page type'),
450
            $this->getPageTypes()
451
        );
452
        $pageClasses->setEmptyString(_t('CMSMain.PAGETYPEANYOPT', 'Any'));
453
454
        // Group the Datefields
455
        $dateGroup = new FieldGroup(
456
            $dateFrom,
457
            $dateTo
458
        );
459
        $dateGroup->setTitle(_t('CMSSearch.PAGEFILTERDATEHEADING', 'Last edited'));
460
461
        // view mode
462
        $viewMode = HiddenField::create('view', false, $this->ViewState());
463
464
        // Create the Field list
465
        $fields = new FieldList(
466
            $content,
467
            $pageFilter,
468
            $pageClasses,
469
            $dateGroup,
470
            $viewMode
471
        );
472
473
        // Create the Search and Reset action
474
        $actions = new FieldList(
475
            FormAction::create('doSearch', _t('CMSMain_left_ss.APPLY_FILTER', 'Search'))
476
                ->addExtraClass('btn btn-primary'),
477
            ResetFormAction::create('clear', _t('CMSMain_left_ss.CLEAR_FILTER', 'Clear'))
478
                ->addExtraClass('btn btn-secondary')
479
        );
480
481
        // Use <button> to allow full jQuery UI styling on the all of the Actions
482
        /** @var FormAction $action */
483
        foreach ($actions->dataFields() as $action) {
484
            /** @var FormAction $action */
485
            $action->setUseButtonTag(true);
486
        }
487
488
        // Create the form
489
        /** @skipUpgrade */
490
        $form = Form::create($this, 'SearchForm', $fields, $actions)
491
            ->addExtraClass('cms-search-form')
492
            ->setFormMethod('GET')
493
            ->setFormAction($this->Link())
494
            ->disableSecurityToken()
495
            ->unsetValidator();
496
497
        // Load the form with previously sent search data
498
        $form->loadDataFrom($this->getRequest()->getVars());
499
500
        // Allow decorators to modify the form
501
        $this->extend('updateSearchForm', $form);
502
503
        return $form;
504
    }
505
506
    /**
507
     * Returns a sorted array suitable for a dropdown with pagetypes and their translated name
508
     *
509
     * @return array
510
     */
511
    protected function getPageTypes()
512
    {
513
        $pageTypes = array();
514
        foreach (SiteTree::page_type_classes() as $pageTypeClass) {
515
            $pageTypes[$pageTypeClass] = SiteTree::singleton($pageTypeClass)->i18n_singular_name();
516
        }
517
        asort($pageTypes);
518
        return $pageTypes;
519
    }
520
521
    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...
522
    {
523
        return $this->getsubtree($this->getRequest());
524
    }
525
526
    /**
527
     * @param bool $unlinked
528
     * @return ArrayList
529
     */
530
    public function Breadcrumbs($unlinked = false)
531
    {
532
        $items = parent::Breadcrumbs($unlinked);
533
534
        if ($items->count() > 1) {
535
            // Specific to the SiteTree admin section, we never show the cms section and current
536
            // page in the same breadcrumbs block.
537
            $items->shift();
538
        }
539
540
        return $items;
541
    }
542
543
    /**
544
     * Create serialized JSON string with site tree hints data to be injected into
545
     * 'data-hints' attribute of root node of jsTree.
546
     *
547
     * @return string Serialized JSON
548
     */
549
    public function SiteTreeHints()
550
    {
551
        $classes = SiteTree::page_type_classes();
552
553
        $cacheCanCreate = array();
554
        foreach ($classes as $class) {
555
            $cacheCanCreate[$class] = singleton($class)->canCreate();
556
        }
557
558
        // Generate basic cache key. Too complex to encompass all variations
559
        $cache = Cache::factory('CMSMain_SiteTreeHints');
560
        $cacheKey = md5(implode('_', array(Member::currentUserID(), implode(',', $cacheCanCreate), implode(',', $classes))));
561
        if ($this->getRequest()->getVar('flush')) {
562
            $cache->clean(Zend_Cache::CLEANING_MODE_ALL);
563
        }
564
        $json = $cache->load($cacheKey);
565
        if (!$json) {
566
            $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...
567
            $def['Root']['disallowedChildren'] = array();
568
569
            // Contains all possible classes to support UI controls listing them all,
570
            // such as the "add page here" context menu.
571
            $def['All'] = array();
572
573
            // Identify disallows and set globals
574
            foreach ($classes as $class) {
575
                $obj = singleton($class);
576
                if ($obj instanceof HiddenClass) {
577
                    continue;
578
                }
579
580
                // Name item
581
                $def['All'][$class] = array(
582
                    'title' => $obj->i18n_singular_name()
583
                );
584
585
                // Check if can be created at the root
586
                $needsPerm = $obj->stat('need_permission');
587
                if (!$obj->stat('can_be_root')
588
                    || (!array_key_exists($class, $cacheCanCreate) || !$cacheCanCreate[$class])
589
                    || ($needsPerm && !$this->can($needsPerm))
590
                ) {
591
                    $def['Root']['disallowedChildren'][] = $class;
592
                }
593
594
                // Hint data specific to the class
595
                $def[$class] = array();
596
597
                $defaultChild = $obj->defaultChild();
598
                if ($defaultChild !== 'Page' && $defaultChild !== null) {
599
                    $def[$class]['defaultChild'] = $defaultChild;
600
                }
601
602
                $defaultParent = $obj->defaultParent();
603
                if ($defaultParent !== 1 && $defaultParent !== null) {
604
                    $def[$class]['defaultParent'] = $defaultParent;
605
                }
606
            }
607
608
            $this->extend('updateSiteTreeHints', $def);
609
610
            $json = Convert::raw2json($def);
611
            $cache->save($json, $cacheKey);
612
        }
613
        return $json;
614
    }
615
616
    /**
617
     * Populates an array of classes in the CMS
618
     * which allows the user to change the page type.
619
     *
620
     * @return SS_List
621
     */
622
    public function PageTypes()
623
    {
624
        $classes = SiteTree::page_type_classes();
625
626
        $result = new ArrayList();
627
628
        foreach ($classes as $class) {
629
            $instance = singleton($class);
630
631
            if ($instance instanceof HiddenClass) {
632
                continue;
633
            }
634
635
            // skip this type if it is restricted
636
            if ($instance->stat('need_permission') && !$this->can(singleton($class)->stat('need_permission'))) {
637
                continue;
638
            }
639
640
            $addAction = $instance->i18n_singular_name();
641
642
            // Get description (convert 'Page' to 'SiteTree' for correct localization lookups)
643
            $i18nClass = ($class == 'Page') ? 'SilverStripe\\CMS\\Model\\SiteTree' : $class;
644
            $description = _t($i18nClass . '.DESCRIPTION');
645
646
            if (!$description) {
647
                $description = $instance->uninherited('description');
648
            }
649
650
            if ($class == 'Page' && !$description) {
651
                $description = SiteTree::singleton()->uninherited('description');
652
            }
653
654
            $result->push(new ArrayData(array(
655
                'ClassName' => $class,
656
                'AddAction' => $addAction,
657
                'Description' => $description,
658
                // TODO Sprite support
659
                'IconURL' => $instance->stat('icon'),
660
                'Title' => singleton($class)->i18n_singular_name(),
661
            )));
662
        }
663
664
        $result = $result->sort('AddAction');
665
666
        return $result;
667
    }
668
669
    /**
670
     * Get a database record to be managed by the CMS.
671
     *
672
     * @param int $id Record ID
673
     * @param int $versionID optional Version id of the given record
674
     * @return SiteTree
675
     */
676
    public function getRecord($id, $versionID = null)
677
    {
678
        $treeClass = $this->stat('tree_class');
679
680
        if ($id instanceof $treeClass) {
681
            return $id;
682
        } elseif ($id && is_numeric($id)) {
683
            $currentStage = Versioned::get_reading_mode();
684
685
            if ($this->getRequest()->getVar('Version')) {
686
                $versionID = (int) $this->getRequest()->getVar('Version');
687
            }
688
689
            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...
690
                $record = Versioned::get_version($treeClass, $id, $versionID);
691
            } else {
692
                $record = DataObject::get_by_id($treeClass, $id);
693
            }
694
695
            // Then, try getting a record from the live site
696
            if (!$record) {
697
                // $record = Versioned::get_one_by_stage($treeClass, "Live", "\"$treeClass\".\"ID\" = $id");
698
                Versioned::set_stage(Versioned::LIVE);
699
                singleton($treeClass)->flushCache();
700
701
                $record = DataObject::get_by_id($treeClass, $id);
702
            }
703
704
            // Then, try getting a deleted record
705
            if (!$record) {
706
                $record = Versioned::get_latest_version($treeClass, $id);
707
            }
708
709
            // Don't open a page from a different locale
710
            /** The record's Locale is saved in database in 2.4, and not related with Session,
711
             *  we should not check their locale matches the Translatable::get_current_locale,
712
             *  here as long as we all the HTTPRequest is init with right locale.
713
             *  This bit breaks the all FileIFrameField functions if the field is used in CMS
714
             *  and its relevent ajax calles, like loading the tree dropdown for TreeSelectorField.
715
             */
716
            /* if($record && SiteTree::has_extension('Translatable') && $record->Locale && $record->Locale != Translatable::get_current_locale()) {
717
				$record = null;
718
			}*/
719
720
            // Set the reading mode back to what it was.
721
            Versioned::set_reading_mode($currentStage);
722
723
            return $record;
724
        } elseif (substr($id, 0, 3) == 'new') {
725
            return $this->getNewItem($id);
726
        }
727
    }
728
729
    /**
730
     * @param int $id
731
     * @param FieldList $fields
732
     * @return Form
733
     */
734
    public function getEditForm($id = null, $fields = null)
735
    {
736
        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...
737
            $id = $this->currentPageID();
738
        }
739
        $form = parent::getEditForm($id, $fields);
740
741
        // TODO Duplicate record fetching (see parent implementation)
742
        $record = $this->getRecord($id);
743
        if ($record && !$record->canView()) {
744
            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 744 which is incompatible with the return type documented by SilverStripe\CMS\Controllers\CMSMain::getEditForm of type SilverStripe\Forms\Form.
Loading history...
745
        }
746
747
        if (!$fields) {
748
            $fields = $form->Fields();
749
        }
750
        $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...
751
752
        if ($record) {
753
            $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...
754
755
            $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...
756
            // Necessary for different subsites
757
            $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...
758
            $fields->push($liveLinkField = new HiddenField("LiveLink"));
759
            $fields->push($stageLinkField = new HiddenField("StageLink"));
760
            $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...
761
762
            if ($record->ID && is_numeric($record->ID)) {
763
                $liveLink = $record->getAbsoluteLiveLink();
764
                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...
765
                    $liveLinkField->setValue($liveLink);
766
                }
767
                if (!$deletedFromStage) {
768
                    $stageLink = Controller::join_links($record->AbsoluteLink(), '?stage=Stage');
769
                    if ($stageLink) {
770
                        $stageLinkField->setValue($stageLink);
771
                    }
772
                }
773
            }
774
775
            // Added in-line to the form, but plucked into different view by LeftAndMain.Preview.js upon load
776
            /** @skipUpgrade */
777
            if ($record instanceof CMSPreviewable && !$fields->fieldByName('SilverStripeNavigator')) {
778
                $navField = new LiteralField('SilverStripeNavigator', $this->getSilverStripeNavigator());
779
                $navField->setAllowHTML(true);
780
                $fields->push($navField);
781
            }
782
783
            // getAllCMSActions can be used to completely redefine the action list
784
            if ($record->hasMethod('getAllCMSActions')) {
785
                $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...
786
            } else {
787
                $actions = $record->getCMSActions();
788
789
                // Find and remove action menus that have no actions.
790
                if ($actions && $actions->count()) {
791
                    /** @var TabSet $tabset */
792
                    $tabset = $actions->fieldByName('ActionMenus');
793
                    if ($tabset) {
794
                        foreach ($tabset->getChildren() as $tab) {
795
                            if (!$tab->getChildren()->count()) {
796
                                $tabset->removeByName($tab->getName());
797
                            }
798
                        }
799
                    }
800
                }
801
            }
802
803
            // Use <button> to allow full jQuery UI styling
804
            $actionsFlattened = $actions->dataFields();
805
            if ($actionsFlattened) {
806
                /** @var FormAction $action */
807
                foreach ($actionsFlattened as $action) {
808
                    $action->setUseButtonTag(true);
809
                }
810
            }
811
812
            if ($record->hasMethod('getCMSValidator')) {
813
                $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...
814
            } else {
815
                $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...
816
            }
817
818
            // TODO Can't merge $FormAttributes in template at the moment
819
            $form->addExtraClass('center ' . $this->BaseCSSClasses());
820
            // Set validation exemptions for specific actions
821
            $form->setValidationExemptActions(array('restore', 'revert', 'deletefromlive', 'delete', 'unpublish', 'rollback', 'doRollback'));
822
823
            // Announce the capability so the frontend can decide whether to allow preview or not.
824
            if ($record instanceof CMSPreviewable) {
825
                $form->addExtraClass('cms-previewable');
826
            }
827
            $form->addExtraClass('fill-height flexbox-area-grow');
828
829
            if (!$record->canEdit() || $deletedFromStage) {
830
                $readonlyFields = $form->Fields()->makeReadonly();
831
                $form->setFields($readonlyFields);
832
            }
833
834
            $form->Fields()->setForm($form);
835
836
            $this->extend('updateEditForm', $form);
837
            return $form;
838
        } elseif ($id) {
839
            $form = Form::create($this, "EditForm", new FieldList(
840
                new LabelField('PageDoesntExistLabel', _t('CMSMain.PAGENOTEXISTS', "This page doesn't exist"))
841
            ), new FieldList())->setHTMLID('Form_EditForm');
842
            return $form;
843
        }
844
    }
845
846
    /**
847
     * @param HTTPRequest $request
848
     * @return string HTML
849
     */
850
    public function treeview($request)
851
    {
852
        return $this->getResponseNegotiator()->respond($request);
853
    }
854
855
    /**
856
     * @param HTTPRequest $request
857
     * @return string HTML
858
     */
859
    public function listview($request)
860
    {
861
        return $this->getResponseNegotiator()->respond($request);
862
    }
863
864
    /**
865
     * @return string
866
     */
867
    public function ViewState()
868
    {
869
        $mode = $this->getRequest()->requestVar('view')
870
            ?: $this->getRequest()->param('Action');
871
        switch ($mode) {
872
            case 'listview':
873
            case 'treeview':
874
                return $mode;
875
            default:
876
                return 'treeview';
877
        }
878
    }
879
880
    /**
881
     * Callback to request the list of page types allowed under a given page instance.
882
     * Provides a slower but more precise response over SiteTreeHints
883
     *
884
     * @param HTTPRequest $request
885
     * @return HTTPResponse
886
     */
887
    public function childfilter($request)
888
    {
889
        // Check valid parent specified
890
        $parentID = $request->requestVar('ParentID');
891
        $parent = SiteTree::get()->byID($parentID);
892
        if (!$parent || !$parent->exists()) {
893
            return $this->httpError(404);
894
        }
895
896
        // Build hints specific to this class
897
        // Identify disallows and set globals
898
        $classes = SiteTree::page_type_classes();
899
        $disallowedChildren = array();
900
        foreach ($classes as $class) {
901
            $obj = singleton($class);
902
            if ($obj instanceof HiddenClass) {
903
                continue;
904
            }
905
906
            if (!$obj->canCreate(null, array('Parent' => $parent))) {
907
                $disallowedChildren[] = $class;
908
            }
909
        }
910
911
        $this->extend('updateChildFilter', $disallowedChildren, $parentID);
912
        return $this
913
            ->getResponse()
914
            ->addHeader('Content-Type', 'application/json; charset=utf-8')
915
            ->setBody(Convert::raw2json($disallowedChildren));
916
    }
917
918
    /**
919
     * Safely reconstruct a selected filter from a given set of query parameters
920
     *
921
     * @param array $params Query parameters to use
922
     * @return CMSSiteTreeFilter The filter class, or null if none present
923
     * @throws InvalidArgumentException if invalid filter class is passed.
924
     */
925
    protected function getQueryFilter($params)
926
    {
927
        if (empty($params['FilterClass'])) {
928
            return null;
929
        }
930
        $filterClass = $params['FilterClass'];
931
        if (!is_subclass_of($filterClass, 'SilverStripe\\CMS\\Controllers\\CMSSiteTreeFilter')) {
932
            throw new InvalidArgumentException("Invalid filter class passed: {$filterClass}");
933
        }
934
        return $filterClass::create($params);
935
    }
936
937
    /**
938
     * Returns the pages meet a certain criteria as {@see CMSSiteTreeFilter} or the subpages of a parent page
939
     * defaulting to no filter and show all pages in first level.
940
     * Doubles as search results, if any search parameters are set through {@link SearchForm()}.
941
     *
942
     * @param array $params Search filter criteria
943
     * @param int $parentID Optional parent node to filter on (can't be combined with other search criteria)
944
     * @return SS_List
945
     * @throws InvalidArgumentException if invalid filter class is passed.
946
     */
947
    public function getList($params = array(), $parentID = 0)
948
    {
949
        if ($filter = $this->getQueryFilter($params)) {
950
            return $filter->getFilteredPages();
951
        } else {
952
            $list = DataList::create($this->stat('tree_class'));
953
            $parentID = is_numeric($parentID) ? $parentID : 0;
954
            return $list->filter("ParentID", $parentID);
955
        }
956
    }
957
958
    /**
959
     * @return Form
960
     */
961
    public function ListViewForm()
962
    {
963
        $params = $this->getRequest()->requestVar('q');
964
        $list = $this->getList($params, $parentID = $this->getRequest()->requestVar('ParentID'));
965
        $gridFieldConfig = GridFieldConfig::create()->addComponents(
966
            new GridFieldSortableHeader(),
967
            new GridFieldDataColumns(),
968
            new GridFieldPaginator(self::config()->page_length)
969
        );
970
        if ($parentID) {
971
            $linkSpec = $this->Link();
972
            $linkSpec = $linkSpec . (strstr($linkSpec, '?') ? '&' : '?') . 'ParentID=%d&view=listview';
973
            $gridFieldConfig->addComponent(
974
                GridFieldLevelup::create($parentID)
975
                    ->setLinkSpec($linkSpec)
976
                    ->setAttributes(array('data-pjax' => 'ListViewForm,Breadcrumbs'))
977
            );
978
        }
979
        $gridField = new GridField('Page', 'Pages', $list, $gridFieldConfig);
980
        /** @var GridFieldDataColumns $columns */
981
        $columns = $gridField->getConfig()->getComponentByType('SilverStripe\\Forms\\GridField\\GridFieldDataColumns');
982
983
        // Don't allow navigating into children nodes on filtered lists
984
        $fields = array(
985
            'getTreeTitle' => _t('SiteTree.PAGETITLE', 'Page Title'),
986
            'singular_name' => _t('SiteTree.PAGETYPE'),
987
            'LastEdited' => _t('SiteTree.LASTUPDATED', 'Last Updated'),
988
        );
989
        /** @var GridFieldSortableHeader $sortableHeader */
990
        $sortableHeader = $gridField->getConfig()->getComponentByType('SilverStripe\\Forms\\GridField\\GridFieldSortableHeader');
991
        $sortableHeader->setFieldSorting(array('getTreeTitle' => 'Title'));
992
        $gridField->getState()->ParentID = $parentID;
993
994
        if (!$params) {
995
            $fields = array_merge(array('listChildrenLink' => ''), $fields);
996
        }
997
998
        $columns->setDisplayFields($fields);
999
        $columns->setFieldCasting(array(
1000
            'Created' => 'DBDatetime->Ago',
1001
            'LastEdited' => 'DBDatetime->FormatFromSettings',
1002
            'getTreeTitle' => 'HTMLFragment'
1003
        ));
1004
1005
        $controller = $this;
1006
        $columns->setFieldFormatting(array(
1007
            'listChildrenLink' => function ($value, &$item) use ($controller) {
1008
                /** @var SiteTree $item */
1009
                $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...
1010
                if ($num) {
1011
                    return sprintf(
1012
                        '<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>',
1013
                        Controller::join_links(
1014
                            $controller->Link(),
1015
                            sprintf("?ParentID=%d&view=listview", (int)$item->ID)
1016
                        ),
1017
                        $num
1018
                    );
1019
                }
1020
            },
1021
            'getTreeTitle' => function ($value, &$item) use ($controller) {
1022
                return sprintf(
1023
                    '<a class="action-detail" href="%s">%s</a>',
1024
                    Controller::join_links(
1025
                        CMSPageEditController::singleton()->Link('show'),
1026
                        (int)$item->ID
1027
                    ),
1028
                    $item->TreeTitle // returns HTML, does its own escaping
1029
                );
1030
            }
1031
        ));
1032
1033
        $negotiator = $this->getResponseNegotiator();
1034
        $listview = Form::create(
1035
            $this,
1036
            'ListViewForm',
1037
            new FieldList($gridField),
1038
            new FieldList()
1039
        )->setHTMLID('Form_ListViewForm');
1040
        $listview->setAttribute('data-pjax-fragment', 'ListViewForm');
1041 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...
1042
            $request = $this->getRequest();
1043
            if ($request->isAjax() && $negotiator) {
1044
                $result = $listview->forTemplate();
1045
                return $negotiator->respond($request, array(
1046
                    'CurrentForm' => function () use ($result) {
1047
                        return $result;
1048
                    }
1049
                ));
1050
            }
1051
        });
1052
1053
        $this->extend('updateListView', $listview);
1054
1055
        $listview->disableSecurityToken();
1056
        return $listview;
1057
    }
1058
1059
    public function currentPageID()
1060
    {
1061
        $id = parent::currentPageID();
1062
1063
        $this->extend('updateCurrentPageID', $id);
1064
1065
        return $id;
1066
    }
1067
1068
    //------------------------------------------------------------------------------------------//
1069
    // Data saving handlers
1070
1071
    /**
1072
     * Save and Publish page handler
1073
     *
1074
     * @param array $data
1075
     * @param Form $form
1076
     * @return HTTPResponse
1077
     * @throws HTTPResponse_Exception
1078
     */
1079
    public function save($data, $form)
1080
    {
1081
        $className = $this->stat('tree_class');
1082
1083
        // Existing or new record?
1084
        $id = $data['ID'];
1085
        if (substr($id, 0, 3) != 'new') {
1086
            /** @var SiteTree $record */
1087
            $record = DataObject::get_by_id($className, $id);
1088
            // Check edit permissions
1089
            if ($record && !$record->canEdit()) {
1090
                return Security::permissionFailure($this);
1091
            }
1092
            if (!$record || !$record->ID) {
1093
                throw new HTTPResponse_Exception("Bad record ID #$id", 404);
1094
            }
1095
        } else {
1096
            if (!$className::singleton()->canCreate()) {
1097
                return Security::permissionFailure($this);
1098
            }
1099
            $record = $this->getNewItem($id, false);
1100
        }
1101
1102
        // Check publishing permissions
1103
        $doPublish = !empty($data['publish']);
1104
        if ($record && $doPublish && !$record->canPublish()) {
1105
            return Security::permissionFailure($this);
1106
        }
1107
1108
        // TODO Coupling to SiteTree
1109
        $record->HasBrokenLink = 0;
1110
        $record->HasBrokenFile = 0;
1111
1112
        if (!$record->ObsoleteClassName) {
1113
            $record->writeWithoutVersion();
1114
        }
1115
1116
        // Update the class instance if necessary
1117
        if (isset($data['ClassName']) && $data['ClassName'] != $record->ClassName) {
1118
            // Replace $record with a new instance of the new class
1119
            $newClassName = $data['ClassName'];
1120
            $record = $record->newClassInstance($newClassName);
1121
        }
1122
1123
        // save form data into record
1124
        $form->saveInto($record);
1125
        $record->write();
1126
1127
        // If the 'Save & Publish' button was clicked, also publish the page
1128
        if ($doPublish) {
1129
            $record->publishRecursive();
1130
            $message = _t(
1131
                'CMSMain.PUBLISHED',
1132
                "Published '{title}' successfully.",
1133
                ['title' => $record->Title]
0 ignored issues
show
Documentation introduced by
array('title' => $record->Title) is of type array<string,?,{"title":"?"}>, but the function expects a string.

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1134
            );
1135
        } else {
1136
            $message = _t(
1137
                'CMSMain.SAVED',
1138
                "Saved '{title}' successfully.",
1139
                ['title' => $record->Title]
0 ignored issues
show
Documentation introduced by
array('title' => $record->Title) is of type array<string,?,{"title":"?"}>, but the function expects a string.

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1140
            );
1141
        }
1142
1143
        $this->getResponse()->addHeader('X-Status', rawurlencode($message));
1144
        return $this->getResponseNegotiator()->respond($this->getRequest());
1145
    }
1146
1147
    /**
1148
     * @uses LeftAndMainExtension->augmentNewSiteTreeItem()
1149
     *
1150
     * @param int|string $id
1151
     * @param bool $setID
1152
     * @return mixed|DataObject
1153
     * @throws HTTPResponse_Exception
1154
     */
1155
    public function getNewItem($id, $setID = true)
1156
    {
1157
        $parentClass = $this->stat('tree_class');
1158
        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...
1159
1160
        if (!is_a($className, $parentClass, true)) {
1161
            $response = Security::permissionFailure($this);
1162
            if (!$response) {
1163
                $response = $this->getResponse();
1164
            }
1165
            throw new HTTPResponse_Exception($response);
1166
        }
1167
1168
        /** @var SiteTree $newItem */
1169
        $newItem = Injector::inst()->create($className);
1170
        if (!$suffix) {
1171
            $sessionTag = "NewItems." . $parentID . "." . $className;
1172
            if (Session::get($sessionTag)) {
1173
                $suffix = '-' . Session::get($sessionTag);
1174
                Session::set($sessionTag, Session::get($sessionTag) + 1);
1175
            } else {
1176
                Session::set($sessionTag, 1);
1177
            }
1178
1179
                $id = $id . $suffix;
1180
        }
1181
1182
        $newItem->Title = _t(
1183
            'CMSMain.NEWPAGE',
1184
            "New {pagetype}",
1185
            'followed by a page type title',
1186
            array('pagetype' => singleton($className)->i18n_singular_name())
1187
        );
1188
        $newItem->ClassName = $className;
1189
        $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...
1190
1191
        // DataObject::fieldExists only checks the current class, not the hierarchy
1192
        // This allows the CMS to set the correct sort value
1193
        if ($newItem->castingHelper('Sort')) {
1194
            $newItem->Sort = DB::prepared_query('SELECT MAX("Sort") FROM "SiteTree" WHERE "ParentID" = ?', array($parentID))->value() + 1;
1195
        }
1196
1197
        if ($setID) {
1198
            $newItem->ID = $id;
1199
        }
1200
1201
        # Some modules like subsites add extra fields that need to be set when the new item is created
1202
        $this->extend('augmentNewSiteTreeItem', $newItem);
1203
1204
        return $newItem;
1205
    }
1206
1207
    /**
1208
     * Actually perform the publication step
1209
     *
1210
     * @param Versioned|DataObject $record
1211
     * @return mixed
1212
     */
1213
    public function performPublish($record)
1214
    {
1215
        if ($record && !$record->canPublish()) {
0 ignored issues
show
Bug introduced by
The method canPublish does only exist in SilverStripe\ORM\Versioning\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...
1216
            return Security::permissionFailure($this);
1217
        }
1218
1219
        $record->publishRecursive();
0 ignored issues
show
Bug introduced by
The method publishRecursive does only exist in SilverStripe\ORM\Versioning\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...
1220
    }
1221
1222
    /**
1223
     * Reverts a page by publishing it to live.
1224
     * Use {@link restorepage()} if you want to restore a page
1225
     * which was deleted from draft without publishing.
1226
     *
1227
     * @uses SiteTree->doRevertToLive()
1228
     *
1229
     * @param array $data
1230
     * @param Form $form
1231
     * @return HTTPResponse
1232
     * @throws HTTPResponse_Exception
1233
     */
1234
    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...
1235
    {
1236
        if (!isset($data['ID'])) {
1237
            throw new HTTPResponse_Exception("Please pass an ID in the form content", 400);
1238
        }
1239
1240
        $id = (int) $data['ID'];
1241
        $restoredPage = Versioned::get_latest_version(SiteTree::class, $id);
1242
        if (!$restoredPage) {
1243
            throw new HTTPResponse_Exception("SiteTree #$id not found", 400);
1244
        }
1245
1246
        /** @var SiteTree $record */
1247
        $record = Versioned::get_one_by_stage('SilverStripe\\CMS\\Model\\SiteTree', 'Live', array(
0 ignored issues
show
Documentation introduced by
array('"SiteTree_Live"."ID"' => $id) is of type array<string,integer,{"\...e\".\"ID\"":"integer"}>, but the function expects a string.

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1248
            '"SiteTree_Live"."ID"' => $id
1249
        ));
1250
1251
        // a user can restore a page without publication rights, as it just adds a new draft state
1252
        // (this action should just be available when page has been "deleted from draft")
1253
        if ($record && !$record->canEdit()) {
1254
            return Security::permissionFailure($this);
1255
        }
1256
        if (!$record || !$record->ID) {
1257
            throw new HTTPResponse_Exception("Bad record ID #$id", 404);
1258
        }
1259
1260
        $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...
1261
1262
        $this->getResponse()->addHeader(
1263
            'X-Status',
1264
            rawurlencode(_t(
1265
                'CMSMain.RESTORED',
1266
                "Restored '{title}' successfully",
1267
                'Param %s is a title',
1268
                array('title' => $record->Title)
1269
            ))
1270
        );
1271
1272
        return $this->getResponseNegotiator()->respond($this->getRequest());
1273
    }
1274
1275
    /**
1276
     * Delete the current page from draft stage.
1277
     *
1278
     * @see deletefromlive()
1279
     *
1280
     * @param array $data
1281
     * @param Form $form
1282
     * @return HTTPResponse
1283
     * @throws HTTPResponse_Exception
1284
     */
1285 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...
1286
    {
1287
        $id = $data['ID'];
1288
        $record = SiteTree::get()->byID($id);
1289
        if ($record && !$record->canDelete()) {
1290
            return Security::permissionFailure();
1291
        }
1292
        if (!$record || !$record->ID) {
1293
            throw new HTTPResponse_Exception("Bad record ID #$id", 404);
1294
        }
1295
1296
        // Delete record
1297
        $record->delete();
1298
1299
        $this->getResponse()->addHeader(
1300
            'X-Status',
1301
            rawurlencode(sprintf(_t('CMSMain.REMOVEDPAGEFROMDRAFT', "Removed '%s' from the draft site"), $record->Title))
1302
        );
1303
1304
        // Even if the record has been deleted from stage and live, it can be viewed in "archive mode"
1305
        return $this->getResponseNegotiator()->respond($this->getRequest());
1306
    }
1307
1308
    /**
1309
     * Delete this page from both live and stage
1310
     *
1311
     * @param array $data
1312
     * @param Form $form
1313
     * @return HTTPResponse
1314
     * @throws HTTPResponse_Exception
1315
     */
1316 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...
1317
    {
1318
        $id = $data['ID'];
1319
        /** @var SiteTree $record */
1320
        $record = SiteTree::get()->byID($id);
1321
        if (!$record || !$record->exists()) {
1322
            throw new HTTPResponse_Exception("Bad record ID #$id", 404);
1323
        }
1324
        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...
1325
            return Security::permissionFailure();
1326
        }
1327
1328
        // Archive record
1329
        $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...
1330
1331
        $this->getResponse()->addHeader(
1332
            'X-Status',
1333
            rawurlencode(sprintf(_t('CMSMain.ARCHIVEDPAGE', "Archived page '%s'"), $record->Title))
1334
        );
1335
1336
        // Even if the record has been deleted from stage and live, it can be viewed in "archive mode"
1337
        return $this->getResponseNegotiator()->respond($this->getRequest());
1338
    }
1339
1340
    public function publish($data, $form)
1341
    {
1342
        $data['publish'] = '1';
1343
1344
        return $this->save($data, $form);
1345
    }
1346
1347
    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...
1348
    {
1349
        $className = $this->stat('tree_class');
1350
        /** @var SiteTree $record */
1351
        $record = DataObject::get_by_id($className, $data['ID']);
1352
1353
        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...
1354
            return Security::permissionFailure($this);
1355
        }
1356
        if (!$record || !$record->ID) {
1357
            throw new HTTPResponse_Exception("Bad record ID #" . (int)$data['ID'], 404);
1358
        }
1359
1360
        $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...
1361
1362
        $this->getResponse()->addHeader(
1363
            'X-Status',
1364
            rawurlencode(_t('CMSMain.REMOVEDPAGE', "Removed '{title}' from the published site", array('title' => $record->Title)))
0 ignored issues
show
Documentation introduced by
array('title' => $record->Title) is of type array<string,string,{"title":"string"}>, but the function expects a string.

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1365
        );
1366
1367
        return $this->getResponseNegotiator()->respond($this->getRequest());
1368
    }
1369
1370
    /**
1371
     * @return HTTPResponse
1372
     */
1373
    public function rollback()
1374
    {
1375
        return $this->doRollback(array(
1376
            'ID' => $this->currentPageID(),
1377
            'Version' => $this->getRequest()->param('VersionID')
1378
        ), 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...
1379
    }
1380
1381
    /**
1382
     * Rolls a site back to a given version ID
1383
     *
1384
     * @param array $data
1385
     * @param Form $form
1386
     * @return HTTPResponse
1387
     */
1388
    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...
1389
    {
1390
        $this->extend('onBeforeRollback', $data['ID']);
1391
1392
        $id = (isset($data['ID'])) ? (int) $data['ID'] : null;
1393
        $version = (isset($data['Version'])) ? (int) $data['Version'] : null;
1394
1395
        /** @var DataObject|Versioned $record */
1396
        $record = DataObject::get_by_id($this->stat('tree_class'), $id);
1397
        if ($record && !$record->canEdit()) {
0 ignored issues
show
Bug introduced by
The call to canEdit() misses a required argument $member.

This check looks for function calls that miss required arguments.

Loading history...
1398
            return Security::permissionFailure($this);
1399
        }
1400
1401
        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...
1402
            $record->doRollbackTo($version);
0 ignored issues
show
Bug introduced by
The method doRollbackTo does only exist in SilverStripe\ORM\Versioning\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...
1403
            $message = _t(
1404
                'CMSMain.ROLLEDBACKVERSIONv2',
1405
                "Rolled back to version #%d.",
1406
                array('version' => $data['Version'])
0 ignored issues
show
Documentation introduced by
array('version' => $data['Version']) is of type array<string,?,{"version":"?"}>, but the function expects a string.

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1407
            );
1408
        } else {
1409
            $record->doRevertToLive();
0 ignored issues
show
Bug introduced by
The method doRevertToLive does only exist in SilverStripe\ORM\Versioning\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...
1410
            $message = _t(
1411
                'CMSMain.ROLLEDBACKPUBv2',
1412
                "Rolled back to published version."
1413
            );
1414
        }
1415
1416
        $this->getResponse()->addHeader('X-Status', rawurlencode($message));
1417
1418
        // Can be used in different contexts: In normal page edit view, in which case the redirect won't have any effect.
1419
        // Or in history view, in which case a revert causes the CMS to re-load the edit view.
1420
        // The X-Pjax header forces a "full" content refresh on redirect.
1421
        $url = Controller::join_links(CMSPageEditController::singleton()->Link('show'), $record->ID);
1422
        $this->getResponse()->addHeader('X-ControllerURL', $url);
1423
        $this->getRequest()->addHeader('X-Pjax', 'Content');
1424
        $this->getResponse()->addHeader('X-Pjax', 'Content');
1425
1426
        return $this->getResponseNegotiator()->respond($this->getRequest());
1427
    }
1428
1429
    /**
1430
     * Batch Actions Handler
1431
     */
1432
    public function batchactions()
1433
    {
1434
        return new CMSBatchActionHandler($this, 'batchactions');
1435
    }
1436
1437
    public function BatchActionParameters()
1438
    {
1439
        $batchActions = CMSBatchActionHandler::config()->batch_actions;
1440
1441
        $forms = array();
1442
        foreach ($batchActions as $urlSegment => $batchAction) {
1443
            $SNG_action = singleton($batchAction);
1444
            if ($SNG_action->canView() && $fieldset = $SNG_action->getParameterFields()) {
1445
                $formHtml = '';
1446
                /** @var FormField $field */
1447
                foreach ($fieldset as $field) {
1448
                    $formHtml .= $field->Field();
1449
                }
1450
                $forms[$urlSegment] = $formHtml;
1451
            }
1452
        }
1453
        $pageHtml = '';
1454
        foreach ($forms as $urlSegment => $html) {
1455
            $pageHtml .= "<div class=\"params\" id=\"BatchActionParameters_$urlSegment\">$html</div>\n\n";
1456
        }
1457
        return new LiteralField("BatchActionParameters", '<div id="BatchActionParameters" style="display:none">'.$pageHtml.'</div>');
1458
    }
1459
    /**
1460
     * Returns a list of batch actions
1461
     */
1462
    public function BatchActionList()
1463
    {
1464
        return $this->batchactions()->batchActionList();
1465
    }
1466
1467
    public function publishall($request)
1468
    {
1469
        if (!Permission::check('ADMIN')) {
1470
            return Security::permissionFailure($this);
1471
        }
1472
1473
        increase_time_limit_to();
1474
        increase_memory_limit_to();
1475
1476
        $response = "";
1477
1478
        if (isset($this->requestParams['confirm'])) {
1479
            // Protect against CSRF on destructive action
1480
            if (!SecurityToken::inst()->checkRequest($request)) {
1481
                return $this->httpError(400);
1482
            }
1483
1484
            $start = 0;
1485
            $pages = SiteTree::get()->limit("$start,30");
1486
            $count = 0;
1487
            while ($pages) {
1488
                /** @var SiteTree $page */
1489
                foreach ($pages as $page) {
1490
                    if ($page && !$page->canPublish()) {
1491
                        return Security::permissionFailure($this);
1492
                    }
1493
1494
                    $page->publishRecursive();
1495
                    $page->destroy();
1496
                    unset($page);
1497
                    $count++;
1498
                    $response .= "<li>$count</li>";
1499
                }
1500
                if ($pages->count() > 29) {
1501
                    $start += 30;
1502
                    $pages = SiteTree::get()->limit("$start,30");
1503
                } else {
1504
                    break;
1505
                }
1506
            }
1507
            $response .= _t('CMSMain.PUBPAGES', "Done: Published {count} pages", array('count' => $count));
0 ignored issues
show
Documentation introduced by
array('count' => $count) is of type array<string,integer,{"count":"integer"}>, but the function expects a string.

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1508
        } else {
1509
            $token = SecurityToken::inst();
1510
            $fields = new FieldList();
1511
            $token->updateFieldSet($fields);
1512
            $tokenField = $fields->first();
1513
            $tokenHtml = ($tokenField) ? $tokenField->FieldHolder() : '';
1514
            $publishAllDescription = _t(
1515
                'CMSMain.PUBALLFUN2',
1516
                'Pressing this button will do the equivalent of going to every page and pressing "publish".  '
1517
                . 'It\'s intended to be used after there have been massive edits of the content, such as when '
1518
                . 'the site was first built.'
1519
            );
1520
            $response .= '<h1>' . _t('CMSMain.PUBALLFUN', '"Publish All" functionality') . '</h1>
1521
				<p>' . $publishAllDescription . '</p>
1522
				<form method="post" action="publishall">
1523
					<input type="submit" name="confirm" value="'
1524
                    . _t('CMSMain.PUBALLCONFIRM', "Please publish every page in the site, copying content stage to live", 'Confirmation button') .'" />'
1525
                    . $tokenHtml .
1526
                '</form>';
1527
        }
1528
1529
        return $response;
1530
    }
1531
1532
    /**
1533
     * Restore a completely deleted page from the SiteTree_versions table.
1534
     *
1535
     * @param array $data
1536
     * @param Form $form
1537
     * @return HTTPResponse
1538
     */
1539
    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...
1540
    {
1541
        if (!isset($data['ID']) || !is_numeric($data['ID'])) {
1542
            return new HTTPResponse("Please pass an ID in the form content", 400);
1543
        }
1544
1545
        $id = (int)$data['ID'];
1546
        /** @var SiteTree $restoredPage */
1547
        $restoredPage = Versioned::get_latest_version(SiteTree::class, $id);
1548
        if (!$restoredPage) {
1549
            return new HTTPResponse("SiteTree #$id not found", 400);
1550
        }
1551
1552
        $restoredPage = $restoredPage->doRestoreToStage();
1553
1554
        $this->getResponse()->addHeader(
1555
            'X-Status',
1556
            rawurlencode(_t(
1557
                'CMSMain.RESTORED',
1558
                "Restored '{title}' successfully",
1559
                array('title' => $restoredPage->Title)
0 ignored issues
show
Documentation introduced by
array('title' => $restoredPage->Title) is of type array<string,string,{"title":"string"}>, but the function expects a string.

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1560
            ))
1561
        );
1562
1563
        return $this->getResponseNegotiator()->respond($this->getRequest());
1564
    }
1565
1566
    public function duplicate($request)
1567
    {
1568
        // Protect against CSRF on destructive action
1569
        if (!SecurityToken::inst()->checkRequest($request)) {
1570
            return $this->httpError(400);
1571
        }
1572
1573
        if (($id = $this->urlParams['ID']) && is_numeric($id)) {
1574
            /** @var SiteTree $page */
1575
            $page = SiteTree::get()->byID($id);
1576 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...
1577
                return Security::permissionFailure($this);
1578
            }
1579
            if (!$page || !$page->ID) {
1580
                throw new HTTPResponse_Exception("Bad record ID #$id", 404);
1581
            }
1582
1583
            $newPage = $page->duplicate();
1584
1585
            // ParentID can be hard-set in the URL.  This is useful for pages with multiple parents
1586
            if (isset($_GET['parentID']) && is_numeric($_GET['parentID'])) {
1587
                $newPage->ParentID = $_GET['parentID'];
1588
                $newPage->write();
1589
            }
1590
1591
            $this->getResponse()->addHeader(
1592
                'X-Status',
1593
                rawurlencode(_t(
1594
                    'CMSMain.DUPLICATED',
1595
                    "Duplicated '{title}' successfully",
1596
                    array('title' => $newPage->Title)
0 ignored issues
show
Documentation introduced by
array('title' => $newPage->Title) is of type array<string,?,{"title":"?"}>, but the function expects a string.

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1597
                ))
1598
            );
1599
            $url = Controller::join_links(CMSPageEditController::singleton()->Link('show'), $newPage->ID);
1600
            $this->getResponse()->addHeader('X-ControllerURL', $url);
1601
            $this->getRequest()->addHeader('X-Pjax', 'Content');
1602
            $this->getResponse()->addHeader('X-Pjax', 'Content');
1603
1604
            return $this->getResponseNegotiator()->respond($this->getRequest());
1605
        } else {
1606
            return new HTTPResponse("CMSMain::duplicate() Bad ID: '$id'", 400);
1607
        }
1608
    }
1609
1610
    public function duplicatewithchildren($request)
1611
    {
1612
        // Protect against CSRF on destructive action
1613
        if (!SecurityToken::inst()->checkRequest($request)) {
1614
            return $this->httpError(400);
1615
        }
1616
        increase_time_limit_to();
1617
        if (($id = $this->urlParams['ID']) && is_numeric($id)) {
1618
            /** @var SiteTree $page */
1619
            $page = SiteTree::get()->byID($id);
1620 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...
1621
                return Security::permissionFailure($this);
1622
            }
1623
            if (!$page || !$page->ID) {
1624
                throw new HTTPResponse_Exception("Bad record ID #$id", 404);
1625
            }
1626
1627
            $newPage = $page->duplicateWithChildren();
1628
1629
            $this->getResponse()->addHeader(
1630
                'X-Status',
1631
                rawurlencode(_t(
1632
                    'CMSMain.DUPLICATEDWITHCHILDREN',
1633
                    "Duplicated '{title}' and children successfully",
1634
                    array('title' => $newPage->Title)
0 ignored issues
show
Documentation introduced by
array('title' => $newPage->Title) is of type array<string,string,{"title":"string"}>, but the function expects a string.

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1635
                ))
1636
            );
1637
            $url = Controller::join_links(CMSPageEditController::singleton()->Link('show'), $newPage->ID);
1638
            $this->getResponse()->addHeader('X-ControllerURL', $url);
1639
            $this->getRequest()->addHeader('X-Pjax', 'Content');
1640
            $this->getResponse()->addHeader('X-Pjax', 'Content');
1641
1642
            return $this->getResponseNegotiator()->respond($this->getRequest());
1643
        } else {
1644
            return new HTTPResponse("CMSMain::duplicatewithchildren() Bad ID: '$id'", 400);
1645
        }
1646
    }
1647
1648
    public function providePermissions()
1649
    {
1650
        $title = CMSPagesController::menu_title();
1651
        return array(
1652
            "CMS_ACCESS_CMSMain" => array(
1653
                'name' => _t('CMSMain.ACCESS', "Access to '{title}' section", array('title' => $title)),
0 ignored issues
show
Documentation introduced by
array('title' => $title) is of type array<string,string,{"title":"string"}>, but the function expects a string.

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1654
                'category' => _t('Permission.CMS_ACCESS_CATEGORY', 'CMS Access'),
1655
                'help' => _t(
1656
                    'CMSMain.ACCESS_HELP',
1657
                    '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".'
1658
                ),
1659
                'sort' => -99 // below "CMS_ACCESS_LeftAndMain", but above everything else
1660
            )
1661
        );
1662
    }
1663
}
1664