Completed
Pull Request — master (#1774)
by
unknown
13:16
created

CMSMain::LinkListView()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

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

This error can happen if you refactor code and forget to move the variable initialization.

Let’s take a look at a simple example:

function someFunction() {
    $x = 5;
    echo $x;
}

The above code is perfectly fine. Now imagine that we re-order the statements:

function someFunction() {
    echo $x;
    $x = 5;
}

In that case, $x would be read before it is initialized. This was a very basic example, however the principle is the same for the found issue.

Loading history...
840
        if (!$descendants) {
0 ignored issues
show
Bug introduced by
The variable $descendants seems only to be defined at a later point. Did you maybe move this code here without moving the variable definition?

This error can happen if you refactor code and forget to move the variable initialization.

Let’s take a look at a simple example:

function someFunction() {
    $x = 5;
    echo $x;
}

The above code is perfectly fine. Now imagine that we re-order the statements:

function someFunction() {
    echo $x;
    $x = 5;
}

In that case, $x would be read before it is initialized. This was a very basic example, however the principle is the same for the found issue.

Loading history...
841
            $descendants = [];
842
        }
843
844
        // Get all campaigns that the page and its descendants belong to
845
        $inChangeSetIDs = ChangeSetItem::get_for_object($record)->column('ChangeSetID');
846
847
        foreach ($descendants as $page) {
0 ignored issues
show
Bug introduced by
The variable $descendants does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
848
            $inChangeSetIDs = array_merge($inChangeSetIDs, ChangeSetItem::get_for_object($page)->column('ChangeSetID'));
849
        }
850
851
        if (count($inChangeSetIDs) > 0) {
852
            $inChangeSets = ChangeSet::get()->filter(['ID' => $inChangeSetIDs, 'State' => ChangeSet::STATE_OPEN]);
853
        } else {
854
            $inChangeSets = new ArrayList();
855
        }
856
857
        $numCampaigns = ChangeSet::singleton()->i18n_pluralise($inChangeSets->count());
858
        $numCampaigns = mb_strtolower($numCampaigns);
859
860
        if (count($descendants) > 0 && $inChangeSets->count() > 0) {
861
            $archiveWarningMsg = _t('CMSMain.ArchiveWarningWithChildrenAndCampaigns', 'Warning: This page and all of its child pages will be unpublished and automatically removed from their associated {NumCampaigns} before being sent to the archive.\n\nAre you sure you want to proceed?', [ 'NumCampaigns' => $numCampaigns ]);
862
        } elseif (count($descendants) > 0) {
863
            $archiveWarningMsg = _t('CMSMain.ArchiveWarningWithChildren', 'Warning: This page and all of its child pages will be unpublished before being sent to the archive.\n\nAre you sure you want to proceed?');
864
        } elseif ($inChangeSets->count() > 0) {
865
            $archiveWarningMsg = _t('CMSMain.ArchiveWarningWithCampaigns', 'Warning: This page will be unpublished and automatically removed from their associated {NumCampaigns} before being sent to the archive.\n\nAre you sure you want to proceed?', [ 'NumCampaigns' => $numCampaigns ]);
866
        } else {
867
            $archiveWarningMsg = _t('CMSMain.ArchiveWarning', 'Warning: This page will be unpublished before being sent to the archive.\n\nAre you sure you want to proceed?');
868
        }
869
870
        return $archiveWarningMsg;
871
    }
872
873
    /**
874
     * @param HTTPRequest $request
875
     * @return string HTML
876
     */
877
    public function treeview($request)
878
    {
879
        return $this->getResponseNegotiator()->respond($request);
880
    }
881
882
    /**
883
     * @param HTTPRequest $request
884
     * @return string HTML
885
     */
886
    public function listview($request)
887
    {
888
        return $this->getResponseNegotiator()->respond($request);
889
    }
890
891
    /**
892
     * @return string
893
     */
894
    public function ViewState()
895
    {
896
        $mode = $this->getRequest()->requestVar('view')
897
            ?: $this->getRequest()->param('Action');
898
        switch ($mode) {
899
            case 'listview':
900
            case 'treeview':
901
                return $mode;
902
            default:
903
                return 'treeview';
904
        }
905
    }
906
907
    /**
908
     * Callback to request the list of page types allowed under a given page instance.
909
     * Provides a slower but more precise response over SiteTreeHints
910
     *
911
     * @param HTTPRequest $request
912
     * @return HTTPResponse
913
     */
914
    public function childfilter($request)
915
    {
916
        // Check valid parent specified
917
        $parentID = $request->requestVar('ParentID');
918
        $parent = SiteTree::get()->byID($parentID);
919
        if (!$parent || !$parent->exists()) {
920
            return $this->httpError(404);
921
        }
922
923
        // Build hints specific to this class
924
        // Identify disallows and set globals
925
        $classes = SiteTree::page_type_classes();
926
        $disallowedChildren = array();
927
        foreach ($classes as $class) {
928
            $obj = singleton($class);
929
            if ($obj instanceof HiddenClass) {
930
                continue;
931
            }
932
933
            if (!$obj->canCreate(null, array('Parent' => $parent))) {
934
                $disallowedChildren[] = $class;
935
            }
936
        }
937
938
        $this->extend('updateChildFilter', $disallowedChildren, $parentID);
939
        return $this
940
            ->getResponse()
941
            ->addHeader('Content-Type', 'application/json; charset=utf-8')
942
            ->setBody(Convert::raw2json($disallowedChildren));
943
    }
944
945
    /**
946
     * Safely reconstruct a selected filter from a given set of query parameters
947
     *
948
     * @param array $params Query parameters to use
949
     * @return CMSSiteTreeFilter The filter class, or null if none present
950
     * @throws InvalidArgumentException if invalid filter class is passed.
951
     */
952
    protected function getQueryFilter($params)
953
    {
954
        if (empty($params['FilterClass'])) {
955
            return null;
956
        }
957
        $filterClass = $params['FilterClass'];
958
        if (!is_subclass_of($filterClass, 'SilverStripe\\CMS\\Controllers\\CMSSiteTreeFilter')) {
959
            throw new InvalidArgumentException("Invalid filter class passed: {$filterClass}");
960
        }
961
        return $filterClass::create($params);
962
    }
963
964
    /**
965
     * Returns the pages meet a certain criteria as {@see CMSSiteTreeFilter} or the subpages of a parent page
966
     * defaulting to no filter and show all pages in first level.
967
     * Doubles as search results, if any search parameters are set through {@link SearchForm()}.
968
     *
969
     * @param array $params Search filter criteria
970
     * @param int $parentID Optional parent node to filter on (can't be combined with other search criteria)
971
     * @return SS_List
972
     * @throws InvalidArgumentException if invalid filter class is passed.
973
     */
974
    public function getList($params = array(), $parentID = 0)
975
    {
976
        if ($filter = $this->getQueryFilter($params)) {
977
            return $filter->getFilteredPages();
978
        } else {
979
            $list = DataList::create($this->stat('tree_class'));
980
            $parentID = is_numeric($parentID) ? $parentID : 0;
981
            return $list->filter("ParentID", $parentID);
982
        }
983
    }
984
985
    /**
986
     * @return Form
987
     */
988
    public function ListViewForm()
989
    {
990
        $params = $this->getRequest()->requestVar('q');
991
        $list = $this->getList($params, $parentID = $this->getRequest()->requestVar('ParentID'));
992
        $gridFieldConfig = GridFieldConfig::create()->addComponents(
993
            new GridFieldSortableHeader(),
994
            new GridFieldDataColumns(),
995
            new GridFieldPaginator($this->config()->get('page_length'))
996
        );
997
        if ($parentID) {
998
            $linkSpec = $this->Link();
999
            $linkSpec = $linkSpec . (strstr($linkSpec, '?') ? '&' : '?') . 'ParentID=%d&view=listview';
1000
            $gridFieldConfig->addComponent(
1001
                GridFieldLevelup::create($parentID)
1002
                    ->setLinkSpec($linkSpec)
1003
                    ->setAttributes(array('data-pjax' => 'ListViewForm,Breadcrumbs'))
1004
            );
1005
        }
1006
        $gridField = new GridField('Page', 'Pages', $list, $gridFieldConfig);
1007
        /** @var GridFieldDataColumns $columns */
1008
        $columns = $gridField->getConfig()->getComponentByType('SilverStripe\\Forms\\GridField\\GridFieldDataColumns');
1009
1010
        // Don't allow navigating into children nodes on filtered lists
1011
        $fields = array(
1012
            'getTreeTitle' => _t('SiteTree.PAGETITLE', 'Page Title'),
1013
            'singular_name' => _t('SiteTree.PAGETYPE', 'Page Type'),
1014
            'LastEdited' => _t('SiteTree.LASTUPDATED', 'Last Updated'),
1015
        );
1016
        /** @var GridFieldSortableHeader $sortableHeader */
1017
        $sortableHeader = $gridField->getConfig()->getComponentByType('SilverStripe\\Forms\\GridField\\GridFieldSortableHeader');
1018
        $sortableHeader->setFieldSorting(array('getTreeTitle' => 'Title'));
1019
        $gridField->getState()->ParentID = $parentID;
1020
1021
        if (!$params) {
1022
            $fields = array_merge(array('listChildrenLink' => ''), $fields);
1023
        }
1024
1025
        $columns->setDisplayFields($fields);
1026
        $columns->setFieldCasting(array(
1027
            'Created' => 'DBDatetime->Ago',
1028
            'LastEdited' => 'DBDatetime->FormatFromSettings',
1029
            'getTreeTitle' => 'HTMLFragment'
1030
        ));
1031
1032
        $controller = $this;
1033
        $columns->setFieldFormatting(array(
1034
            'listChildrenLink' => function ($value, &$item) use ($controller) {
1035
                /** @var SiteTree $item */
1036
                $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...
1037
                if ($num) {
1038
                    return sprintf(
1039
                        '<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>',
1040
                        Controller::join_links(
1041
                            $controller->Link(),
1042
                            sprintf("?ParentID=%d&view=listview", (int)$item->ID)
1043
                        ),
1044
                        $num
1045
                    );
1046
                }
1047
            },
1048
            'getTreeTitle' => function ($value, &$item) use ($controller) {
1049
                return sprintf(
1050
                    '<a class="action-detail" href="%s">%s</a>',
1051
                    Controller::join_links(
1052
                        CMSPageEditController::singleton()->Link('show'),
1053
                        (int)$item->ID
1054
                    ),
1055
                    $item->TreeTitle // returns HTML, does its own escaping
1056
                );
1057
            }
1058
        ));
1059
1060
        $negotiator = $this->getResponseNegotiator();
1061
        $listview = Form::create(
1062
            $this,
1063
            'ListViewForm',
1064
            new FieldList($gridField),
1065
            new FieldList()
1066
        )->setHTMLID('Form_ListViewForm');
1067
        $listview->setAttribute('data-pjax-fragment', 'ListViewForm');
1068 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...
1069
            $request = $this->getRequest();
1070
            if ($request->isAjax() && $negotiator) {
1071
                $result = $listview->forTemplate();
1072
                return $negotiator->respond($request, array(
1073
                    'CurrentForm' => function () use ($result) {
1074
                        return $result;
1075
                    }
1076
                ));
1077
            }
1078
        });
1079
1080
        $this->extend('updateListView', $listview);
1081
1082
        $listview->disableSecurityToken();
1083
        return $listview;
1084
    }
1085
1086
    public function currentPageID()
1087
    {
1088
        $id = parent::currentPageID();
1089
1090
        $this->extend('updateCurrentPageID', $id);
1091
1092
        return $id;
1093
    }
1094
1095
    //------------------------------------------------------------------------------------------//
1096
    // Data saving handlers
1097
1098
    /**
1099
     * Save and Publish page handler
1100
     *
1101
     * @param array $data
1102
     * @param Form $form
1103
     * @return HTTPResponse
1104
     * @throws HTTPResponse_Exception
1105
     */
1106
    public function save($data, $form)
1107
    {
1108
        $className = $this->stat('tree_class');
1109
1110
        // Existing or new record?
1111
        $id = $data['ID'];
1112
        if (substr($id, 0, 3) != 'new') {
1113
            /** @var SiteTree $record */
1114
            $record = DataObject::get_by_id($className, $id);
1115
            // Check edit permissions
1116
            if ($record && !$record->canEdit()) {
1117
                return Security::permissionFailure($this);
1118
            }
1119
            if (!$record || !$record->ID) {
1120
                throw new HTTPResponse_Exception("Bad record ID #$id", 404);
1121
            }
1122
        } else {
1123
            if (!$className::singleton()->canCreate()) {
1124
                return Security::permissionFailure($this);
1125
            }
1126
            $record = $this->getNewItem($id, false);
1127
        }
1128
1129
        // Check publishing permissions
1130
        $doPublish = !empty($data['publish']);
1131
        if ($record && $doPublish && !$record->canPublish()) {
1132
            return Security::permissionFailure($this);
1133
        }
1134
1135
        // TODO Coupling to SiteTree
1136
        $record->HasBrokenLink = 0;
1137
        $record->HasBrokenFile = 0;
1138
1139
        if (!$record->ObsoleteClassName) {
1140
            $record->writeWithoutVersion();
1141
        }
1142
1143
        // Update the class instance if necessary
1144
        if (isset($data['ClassName']) && $data['ClassName'] != $record->ClassName) {
1145
            // Replace $record with a new instance of the new class
1146
            $newClassName = $data['ClassName'];
1147
            $record = $record->newClassInstance($newClassName);
1148
        }
1149
1150
        // save form data into record
1151
        $form->saveInto($record);
1152
        $record->write();
1153
1154
        // If the 'Save & Publish' button was clicked, also publish the page
1155
        if ($doPublish) {
1156
            $record->publishRecursive();
1157
            $message = _t(
1158
                'CMSMain.PUBLISHED',
1159
                "Published '{title}' successfully.",
1160
                ['title' => $record->Title]
1161
            );
1162
        } else {
1163
            $message = _t(
1164
                'CMSMain.SAVED',
1165
                "Saved '{title}' successfully.",
1166
                ['title' => $record->Title]
1167
            );
1168
        }
1169
1170
        $this->getResponse()->addHeader('X-Status', rawurlencode($message));
1171
        return $this->getResponseNegotiator()->respond($this->getRequest());
1172
    }
1173
1174
    /**
1175
     * @uses LeftAndMainExtension->augmentNewSiteTreeItem()
1176
     *
1177
     * @param int|string $id
1178
     * @param bool $setID
1179
     * @return mixed|DataObject
1180
     * @throws HTTPResponse_Exception
1181
     */
1182
    public function getNewItem($id, $setID = true)
1183
    {
1184
        $parentClass = $this->stat('tree_class');
1185
        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...
1186
1187
        if (!is_a($className, $parentClass, true)) {
1188
            $response = Security::permissionFailure($this);
1189
            if (!$response) {
1190
                $response = $this->getResponse();
1191
            }
1192
            throw new HTTPResponse_Exception($response);
1193
        }
1194
1195
        /** @var SiteTree $newItem */
1196
        $newItem = Injector::inst()->create($className);
1197
        if (!$suffix) {
1198
            $sessionTag = "NewItems." . $parentID . "." . $className;
1199
            if (Session::get($sessionTag)) {
1200
                $suffix = '-' . Session::get($sessionTag);
1201
                Session::set($sessionTag, Session::get($sessionTag) + 1);
1202
            } else {
1203
                Session::set($sessionTag, 1);
1204
            }
1205
1206
                $id = $id . $suffix;
1207
        }
1208
1209
        $newItem->Title = _t(
1210
            'CMSMain.NEWPAGE',
1211
            "New {pagetype}",
1212
            'followed by a page type title',
1213
            array('pagetype' => singleton($className)->i18n_singular_name())
1214
        );
1215
        $newItem->ClassName = $className;
1216
        $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...
1217
1218
        // DataObject::fieldExists only checks the current class, not the hierarchy
1219
        // This allows the CMS to set the correct sort value
1220
        if ($newItem->castingHelper('Sort')) {
1221
            $newItem->Sort = DB::prepared_query('SELECT MAX("Sort") FROM "SiteTree" WHERE "ParentID" = ?', array($parentID))->value() + 1;
1222
        }
1223
1224
        if ($setID) {
1225
            $newItem->ID = $id;
1226
        }
1227
1228
        # Some modules like subsites add extra fields that need to be set when the new item is created
1229
        $this->extend('augmentNewSiteTreeItem', $newItem);
1230
1231
        return $newItem;
1232
    }
1233
1234
    /**
1235
     * Actually perform the publication step
1236
     *
1237
     * @param Versioned|DataObject $record
1238
     * @return mixed
1239
     */
1240
    public function performPublish($record)
1241
    {
1242
        if ($record && !$record->canPublish()) {
0 ignored issues
show
Bug introduced by
The method canPublish does only exist in SilverStripe\Versioned\Versioned, but not in SilverStripe\ORM\DataObject.

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

Let’s take a look at an example:

class A
{
    public function foo() { }
}

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

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

Available Fixes

  1. Add an additional type-check:

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

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

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

Let’s take a look at an example:

class A
{
    public function foo() { }
}

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

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

Available Fixes

  1. Add an additional type-check:

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

    function someFunction(B $x) { /** ... */ }
    
Loading history...
1247
    }
1248
1249
    /**
1250
     * Reverts a page by publishing it to live.
1251
     * Use {@link restorepage()} if you want to restore a page
1252
     * which was deleted from draft without publishing.
1253
     *
1254
     * @uses SiteTree->doRevertToLive()
1255
     *
1256
     * @param array $data
1257
     * @param Form $form
1258
     * @return HTTPResponse
1259
     * @throws HTTPResponse_Exception
1260
     */
1261
    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...
1262
    {
1263
        if (!isset($data['ID'])) {
1264
            throw new HTTPResponse_Exception("Please pass an ID in the form content", 400);
1265
        }
1266
1267
        $id = (int) $data['ID'];
1268
        $restoredPage = Versioned::get_latest_version(SiteTree::class, $id);
1269
        if (!$restoredPage) {
1270
            throw new HTTPResponse_Exception("SiteTree #$id not found", 400);
1271
        }
1272
1273
        /** @var SiteTree $record */
1274
        $record = Versioned::get_one_by_stage('SilverStripe\\CMS\\Model\\SiteTree', 'Live', array(
1275
            '"SiteTree_Live"."ID"' => $id
1276
        ));
1277
1278
        // a user can restore a page without publication rights, as it just adds a new draft state
1279
        // (this action should just be available when page has been "deleted from draft")
1280
        if ($record && !$record->canEdit()) {
1281
            return Security::permissionFailure($this);
1282
        }
1283
        if (!$record || !$record->ID) {
1284
            throw new HTTPResponse_Exception("Bad record ID #$id", 404);
1285
        }
1286
1287
        $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...
1288
1289
        $this->getResponse()->addHeader(
1290
            'X-Status',
1291
            rawurlencode(_t(
1292
                'CMSMain.RESTORED',
1293
                "Restored '{title}' successfully",
1294
                'Param %s is a title',
1295
                array('title' => $record->Title)
1296
            ))
1297
        );
1298
1299
        return $this->getResponseNegotiator()->respond($this->getRequest());
1300
    }
1301
1302
    /**
1303
     * Delete the current page from draft stage.
1304
     *
1305
     * @see deletefromlive()
1306
     *
1307
     * @param array $data
1308
     * @param Form $form
1309
     * @return HTTPResponse
1310
     * @throws HTTPResponse_Exception
1311
     */
1312 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...
1313
    {
1314
        $id = $data['ID'];
1315
        $record = SiteTree::get()->byID($id);
1316
        if ($record && !$record->canDelete()) {
1317
            return Security::permissionFailure();
1318
        }
1319
        if (!$record || !$record->ID) {
1320
            throw new HTTPResponse_Exception("Bad record ID #$id", 404);
1321
        }
1322
1323
        // Delete record
1324
        $record->delete();
1325
1326
        $this->getResponse()->addHeader(
1327
            'X-Status',
1328
            rawurlencode(sprintf(_t('CMSMain.REMOVEDPAGEFROMDRAFT', "Removed '%s' from the draft site"), $record->Title))
1329
        );
1330
1331
        // Even if the record has been deleted from stage and live, it can be viewed in "archive mode"
1332
        return $this->getResponseNegotiator()->respond($this->getRequest());
1333
    }
1334
1335
    /**
1336
     * Delete this page from both live and stage
1337
     *
1338
     * @param array $data
1339
     * @param Form $form
1340
     * @return HTTPResponse
1341
     * @throws HTTPResponse_Exception
1342
     */
1343 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...
1344
    {
1345
        $id = $data['ID'];
1346
        /** @var SiteTree $record */
1347
        $record = SiteTree::get()->byID($id);
1348
        if (!$record || !$record->exists()) {
1349
            throw new HTTPResponse_Exception("Bad record ID #$id", 404);
1350
        }
1351
        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...
1352
            return Security::permissionFailure();
1353
        }
1354
1355
        // Archive record
1356
        $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...
1357
1358
        $this->getResponse()->addHeader(
1359
            'X-Status',
1360
            rawurlencode(sprintf(_t('CMSMain.ARCHIVEDPAGE', "Archived page '%s'"), $record->Title))
1361
        );
1362
1363
        // Even if the record has been deleted from stage and live, it can be viewed in "archive mode"
1364
        return $this->getResponseNegotiator()->respond($this->getRequest());
1365
    }
1366
1367
    public function publish($data, $form)
1368
    {
1369
        $data['publish'] = '1';
1370
1371
        return $this->save($data, $form);
1372
    }
1373
1374
    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...
1375
    {
1376
        $className = $this->stat('tree_class');
1377
        /** @var SiteTree $record */
1378
        $record = DataObject::get_by_id($className, $data['ID']);
1379
1380
        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...
1381
            return Security::permissionFailure($this);
1382
        }
1383
        if (!$record || !$record->ID) {
1384
            throw new HTTPResponse_Exception("Bad record ID #" . (int)$data['ID'], 404);
1385
        }
1386
1387
        $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...
1388
1389
        $this->getResponse()->addHeader(
1390
            'X-Status',
1391
            rawurlencode(_t('CMSMain.REMOVEDPAGE', "Removed '{title}' from the published site", array('title' => $record->Title)))
1392
        );
1393
1394
        return $this->getResponseNegotiator()->respond($this->getRequest());
1395
    }
1396
1397
    /**
1398
     * @return HTTPResponse
1399
     */
1400
    public function rollback()
1401
    {
1402
        return $this->doRollback(array(
1403
            'ID' => $this->currentPageID(),
1404
            'Version' => $this->getRequest()->param('VersionID')
1405
        ), 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...
1406
    }
1407
1408
    /**
1409
     * Rolls a site back to a given version ID
1410
     *
1411
     * @param array $data
1412
     * @param Form $form
1413
     * @return HTTPResponse
1414
     */
1415
    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...
1416
    {
1417
        $this->extend('onBeforeRollback', $data['ID']);
1418
1419
        $id = (isset($data['ID'])) ? (int) $data['ID'] : null;
1420
        $version = (isset($data['Version'])) ? (int) $data['Version'] : null;
1421
1422
        /** @var DataObject|Versioned $record */
1423
        $record = DataObject::get_by_id($this->stat('tree_class'), $id);
1424
        if ($record && !$record->canEdit()) {
0 ignored issues
show
Bug introduced by
The method canEdit does only exist in SilverStripe\ORM\DataObject, but not in SilverStripe\Versioned\Versioned.

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

Let’s take a look at an example:

class A
{
    public function foo() { }
}

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

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

Available Fixes

  1. Add an additional type-check:

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

    function someFunction(B $x) { /** ... */ }
    
Loading history...
1425
            return Security::permissionFailure($this);
1426
        }
1427
1428
        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...
1429
            $record->doRollbackTo($version);
0 ignored issues
show
Bug introduced by
The method doRollbackTo does only exist in SilverStripe\Versioned\Versioned, but not in SilverStripe\ORM\DataObject.

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

Let’s take a look at an example:

class A
{
    public function foo() { }
}

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

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

Available Fixes

  1. Add an additional type-check:

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

    function someFunction(B $x) { /** ... */ }
    
Loading history...
1430
            $message = _t(
1431
                'CMSMain.ROLLEDBACKVERSIONv2',
1432
                "Rolled back to version #%d.",
1433
                array('version' => $data['Version'])
1434
            );
1435
        } else {
1436
            $record->doRevertToLive();
0 ignored issues
show
Bug introduced by
The method doRevertToLive does only exist in SilverStripe\Versioned\Versioned, but not in SilverStripe\ORM\DataObject.

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

Let’s take a look at an example:

class A
{
    public function foo() { }
}

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

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

Available Fixes

  1. Add an additional type-check:

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

    function someFunction(B $x) { /** ... */ }
    
Loading history...
1437
            $message = _t(
1438
                'CMSMain.ROLLEDBACKPUBv2',
1439
                "Rolled back to published version."
1440
            );
1441
        }
1442
1443
        $this->getResponse()->addHeader('X-Status', rawurlencode($message));
1444
1445
        // Can be used in different contexts: In normal page edit view, in which case the redirect won't have any effect.
1446
        // Or in history view, in which case a revert causes the CMS to re-load the edit view.
1447
        // The X-Pjax header forces a "full" content refresh on redirect.
1448
        $url = Controller::join_links(CMSPageEditController::singleton()->Link('show'), $record->ID);
1449
        $this->getResponse()->addHeader('X-ControllerURL', $url);
1450
        $this->getRequest()->addHeader('X-Pjax', 'Content');
1451
        $this->getResponse()->addHeader('X-Pjax', 'Content');
1452
1453
        return $this->getResponseNegotiator()->respond($this->getRequest());
1454
    }
1455
1456
    /**
1457
     * Batch Actions Handler
1458
     */
1459
    public function batchactions()
1460
    {
1461
        return new CMSBatchActionHandler($this, 'batchactions');
1462
    }
1463
1464
    public function BatchActionParameters()
1465
    {
1466
        $batchActions = CMSBatchActionHandler::config()->batch_actions;
1467
1468
        $forms = array();
1469
        foreach ($batchActions as $urlSegment => $batchAction) {
1470
            $SNG_action = singleton($batchAction);
1471
            if ($SNG_action->canView() && $fieldset = $SNG_action->getParameterFields()) {
1472
                $formHtml = '';
1473
                /** @var FormField $field */
1474
                foreach ($fieldset as $field) {
1475
                    $formHtml .= $field->Field();
1476
                }
1477
                $forms[$urlSegment] = $formHtml;
1478
            }
1479
        }
1480
        $pageHtml = '';
1481
        foreach ($forms as $urlSegment => $html) {
1482
            $pageHtml .= "<div class=\"params\" id=\"BatchActionParameters_$urlSegment\">$html</div>\n\n";
1483
        }
1484
        return new LiteralField("BatchActionParameters", '<div id="BatchActionParameters" style="display:none">'.$pageHtml.'</div>');
1485
    }
1486
    /**
1487
     * Returns a list of batch actions
1488
     */
1489
    public function BatchActionList()
1490
    {
1491
        return $this->batchactions()->batchActionList();
1492
    }
1493
1494
    public function publishall($request)
1495
    {
1496
        if (!Permission::check('ADMIN')) {
1497
            return Security::permissionFailure($this);
1498
        }
1499
1500
        increase_time_limit_to();
1501
        increase_memory_limit_to();
1502
1503
        $response = "";
1504
1505
        if (isset($this->requestParams['confirm'])) {
1506
            // Protect against CSRF on destructive action
1507
            if (!SecurityToken::inst()->checkRequest($request)) {
1508
                return $this->httpError(400);
1509
            }
1510
1511
            $start = 0;
1512
            $pages = SiteTree::get()->limit("$start,30");
1513
            $count = 0;
1514
            while ($pages) {
1515
                /** @var SiteTree $page */
1516
                foreach ($pages as $page) {
1517
                    if ($page && !$page->canPublish()) {
1518
                        return Security::permissionFailure($this);
1519
                    }
1520
1521
                    $page->publishRecursive();
1522
                    $page->destroy();
1523
                    unset($page);
1524
                    $count++;
1525
                    $response .= "<li>$count</li>";
1526
                }
1527
                if ($pages->count() > 29) {
1528
                    $start += 30;
1529
                    $pages = SiteTree::get()->limit("$start,30");
1530
                } else {
1531
                    break;
1532
                }
1533
            }
1534
            $response .= _t('CMSMain.PUBPAGES', "Done: Published {count} pages", array('count' => $count));
1535
        } else {
1536
            $token = SecurityToken::inst();
1537
            $fields = new FieldList();
1538
            $token->updateFieldSet($fields);
1539
            $tokenField = $fields->first();
1540
            $tokenHtml = ($tokenField) ? $tokenField->FieldHolder() : '';
1541
            $publishAllDescription = _t(
1542
                'CMSMain.PUBALLFUN2',
1543
                'Pressing this button will do the equivalent of going to every page and pressing "publish".  '
1544
                . 'It\'s intended to be used after there have been massive edits of the content, such as when '
1545
                . 'the site was first built.'
1546
            );
1547
            $response .= '<h1>' . _t('CMSMain.PUBALLFUN', '"Publish All" functionality') . '</h1>
1548
				<p>' . $publishAllDescription . '</p>
1549
				<form method="post" action="publishall">
1550
					<input type="submit" name="confirm" value="'
1551
                    . _t('CMSMain.PUBALLCONFIRM', "Please publish every page in the site, copying content stage to live", 'Confirmation button') .'" />'
1552
                    . $tokenHtml .
1553
                '</form>';
1554
        }
1555
1556
        return $response;
1557
    }
1558
1559
    /**
1560
     * Restore a completely deleted page from the SiteTree_versions table.
1561
     *
1562
     * @param array $data
1563
     * @param Form $form
1564
     * @return HTTPResponse
1565
     */
1566
    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...
1567
    {
1568
        if (!isset($data['ID']) || !is_numeric($data['ID'])) {
1569
            return new HTTPResponse("Please pass an ID in the form content", 400);
1570
        }
1571
1572
        $id = (int)$data['ID'];
1573
        /** @var SiteTree $restoredPage */
1574
        $restoredPage = Versioned::get_latest_version(SiteTree::class, $id);
1575
        if (!$restoredPage) {
1576
            return new HTTPResponse("SiteTree #$id not found", 400);
1577
        }
1578
1579
        $restoredPage = $restoredPage->doRestoreToStage();
1580
1581
        $this->getResponse()->addHeader(
1582
            'X-Status',
1583
            rawurlencode(_t(
1584
                'CMSMain.RESTORED',
1585
                "Restored '{title}' successfully",
1586
                array('title' => $restoredPage->Title)
1587
            ))
1588
        );
1589
1590
        return $this->getResponseNegotiator()->respond($this->getRequest());
1591
    }
1592
1593
    public function duplicate($request)
1594
    {
1595
        // Protect against CSRF on destructive action
1596
        if (!SecurityToken::inst()->checkRequest($request)) {
1597
            return $this->httpError(400);
1598
        }
1599
1600
        if (($id = $this->urlParams['ID']) && is_numeric($id)) {
1601
            /** @var SiteTree $page */
1602
            $page = SiteTree::get()->byID($id);
1603 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...
1604
                return Security::permissionFailure($this);
1605
            }
1606
            if (!$page || !$page->ID) {
1607
                throw new HTTPResponse_Exception("Bad record ID #$id", 404);
1608
            }
1609
1610
            $newPage = $page->duplicate();
1611
1612
            // ParentID can be hard-set in the URL.  This is useful for pages with multiple parents
1613
            if (isset($_GET['parentID']) && is_numeric($_GET['parentID'])) {
1614
                $newPage->ParentID = $_GET['parentID'];
1615
                $newPage->write();
1616
            }
1617
1618
            $this->getResponse()->addHeader(
1619
                'X-Status',
1620
                rawurlencode(_t(
1621
                    'CMSMain.DUPLICATED',
1622
                    "Duplicated '{title}' successfully",
1623
                    array('title' => $newPage->Title)
1624
                ))
1625
            );
1626
            $url = Controller::join_links(CMSPageEditController::singleton()->Link('show'), $newPage->ID);
1627
            $this->getResponse()->addHeader('X-ControllerURL', $url);
1628
            $this->getRequest()->addHeader('X-Pjax', 'Content');
1629
            $this->getResponse()->addHeader('X-Pjax', 'Content');
1630
1631
            return $this->getResponseNegotiator()->respond($this->getRequest());
1632
        } else {
1633
            return new HTTPResponse("CMSMain::duplicate() Bad ID: '$id'", 400);
1634
        }
1635
    }
1636
1637
    public function duplicatewithchildren($request)
1638
    {
1639
        // Protect against CSRF on destructive action
1640
        if (!SecurityToken::inst()->checkRequest($request)) {
1641
            return $this->httpError(400);
1642
        }
1643
        increase_time_limit_to();
1644
        if (($id = $this->urlParams['ID']) && is_numeric($id)) {
1645
            /** @var SiteTree $page */
1646
            $page = SiteTree::get()->byID($id);
1647 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...
1648
                return Security::permissionFailure($this);
1649
            }
1650
            if (!$page || !$page->ID) {
1651
                throw new HTTPResponse_Exception("Bad record ID #$id", 404);
1652
            }
1653
1654
            $newPage = $page->duplicateWithChildren();
1655
1656
            $this->getResponse()->addHeader(
1657
                'X-Status',
1658
                rawurlencode(_t(
1659
                    'CMSMain.DUPLICATEDWITHCHILDREN',
1660
                    "Duplicated '{title}' and children successfully",
1661
                    array('title' => $newPage->Title)
1662
                ))
1663
            );
1664
            $url = Controller::join_links(CMSPageEditController::singleton()->Link('show'), $newPage->ID);
1665
            $this->getResponse()->addHeader('X-ControllerURL', $url);
1666
            $this->getRequest()->addHeader('X-Pjax', 'Content');
1667
            $this->getResponse()->addHeader('X-Pjax', 'Content');
1668
1669
            return $this->getResponseNegotiator()->respond($this->getRequest());
1670
        } else {
1671
            return new HTTPResponse("CMSMain::duplicatewithchildren() Bad ID: '$id'", 400);
1672
        }
1673
    }
1674
1675
    public function providePermissions()
1676
    {
1677
        $title = CMSPagesController::menu_title();
1678
        return array(
1679
            "CMS_ACCESS_CMSMain" => array(
1680
                'name' => _t('CMSMain.ACCESS', "Access to '{title}' section", array('title' => $title)),
1681
                'category' => _t('Permission.CMS_ACCESS_CATEGORY', 'CMS Access'),
1682
                'help' => _t(
1683
                    'CMSMain.ACCESS_HELP',
1684
                    '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".'
1685
                ),
1686
                'sort' => -99 // below "CMS_ACCESS_LeftAndMain", but above everything else
1687
            )
1688
        );
1689
    }
1690
}
1691