Completed
Pull Request — master (#1688)
by Sam
15:41
created

CMSMain::duplicatewithchildren()   D

Complexity

Conditions 9
Paths 5

Size

Total Lines 37
Code Lines 24

Duplication

Lines 3
Ratio 8.11 %

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 3
loc 37
rs 4.909
cc 9
eloc 24
nc 5
nop 1
1
<?php
2
3
namespace SilverStripe\CMS\Controllers;
4
5
use SilverStripe\Admin\AdminRootController;
6
use SilverStripe\Admin\CMSBatchActionHandler;
7
use SilverStripe\Admin\CMSPreviewable;
8
use SilverStripe\Admin\LeftAndMain;
9
use SilverStripe\CMS\BatchActions\CMSBatchAction_Archive;
10
use SilverStripe\CMS\BatchActions\CMSBatchAction_Publish;
11
use SilverStripe\CMS\BatchActions\CMSBatchAction_Restore;
12
use SilverStripe\CMS\BatchActions\CMSBatchAction_Unpublish;
13
use SilverStripe\CMS\Model\CurrentPageIdentifier;
14
use SilverStripe\CMS\Model\RedirectorPage;
15
use SilverStripe\CMS\Model\SiteTree;
16
use SilverStripe\Control\Controller;
17
use SilverStripe\Control\Director;
18
use SilverStripe\Control\Session;
19
use SilverStripe\Control\HTTPRequest;
20
use SilverStripe\Control\HTTPResponse;
21
use SilverStripe\Control\HTTPResponse_Exception;
22
use SilverStripe\Core\Convert;
23
use SilverStripe\Core\Injector\Injector;
24
use SilverStripe\Core\Cache;
25
use SilverStripe\Forms\DateField;
26
use SilverStripe\Forms\DropdownField;
27
use SilverStripe\Forms\FieldGroup;
28
use SilverStripe\Forms\FieldList;
29
use SilverStripe\Forms\Form;
30
use SilverStripe\Forms\FormAction;
31
use SilverStripe\Forms\FormField;
32
use SilverStripe\Forms\GridField\GridField;
33
use SilverStripe\Forms\GridField\GridFieldConfig;
34
use SilverStripe\Forms\GridField\GridFieldDataColumns;
35
use SilverStripe\Forms\GridField\GridFieldLevelup;
36
use SilverStripe\Forms\GridField\GridFieldPaginator;
37
use SilverStripe\Forms\GridField\GridFieldSortableHeader;
38
use SilverStripe\Forms\HiddenField;
39
use SilverStripe\Forms\LabelField;
40
use SilverStripe\Forms\LiteralField;
41
use SilverStripe\Forms\RequiredFields;
42
use SilverStripe\Forms\ResetFormAction;
43
use SilverStripe\Forms\TabSet;
44
use SilverStripe\Forms\TextField;
45
use SilverStripe\ORM\ArrayList;
46
use SilverStripe\ORM\DataList;
47
use SilverStripe\ORM\DataObject;
48
use SilverStripe\ORM\DB;
49
use SilverStripe\ORM\FieldType\DBHTMLText;
50
use SilverStripe\ORM\HiddenClass;
51
use SilverStripe\ORM\SS_List;
52
use SilverStripe\ORM\Versioning\Versioned;
53
use SilverStripe\Security\Member;
54
use SilverStripe\Security\Permission;
55
use SilverStripe\Security\PermissionProvider;
56
use SilverStripe\Security\Security;
57
use SilverStripe\Security\SecurityToken;
58
use SilverStripe\View\ArrayData;
59
use SilverStripe\View\Requirements;
60
use Translatable;
61
use Page;
62
use Zend_Cache;
63
use InvalidArgumentException;
64
65
/**
66
 * The main "content" area of the CMS.
67
 *
68
 * This class creates a 2-frame layout - left-tree and right-form - to sit beneath the main
69
 * admin menu.
70
 *
71
 * @todo Create some base classes to contain the generic functionality that will be replicated.
72
 *
73
 * @mixin LeftAndMainPageIconsExtension
74
 */
75
class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionProvider
76
{
77
78
    private static $url_segment = 'pages';
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
79
80
    private static $url_rule = '/$Action/$ID/$OtherID';
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
81
82
    // Maintain a lower priority than other administration sections
83
    // so that Director does not think they are actions of CMSMain
84
    private static $url_priority = 39;
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
85
86
    private static $menu_title = 'Edit Page';
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
87
88
    private static $menu_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...
89
90
    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...
91
92
    private static $subitem_class = Member::class;
93
94
    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...
95
96
    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...
97
98
    /**
99
     * Amount of results showing on a single page.
100
     *
101
     * @config
102
     * @var int
103
     */
104
    private static $page_length = 15;
105
106
    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...
107
        'archive',
108
        'deleteitems',
109
        'DeleteItemsForm',
110
        'dialog',
111
        'duplicate',
112
        'duplicatewithchildren',
113
        'publishall',
114
        'publishitems',
115
        'PublishItemsForm',
116
        'submit',
117
        'EditForm',
118
        'SearchForm',
119
        'SiteTreeAsUL',
120
        'getshowdeletedsubtree',
121
        'batchactions',
122
        'treeview',
123
        'listview',
124
        'ListViewForm',
125
        'childfilter',
126
    );
127
128
    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...
129
        'TreeIsFiltered' => 'Boolean',
130
        'AddForm' => 'HTMLFragment',
131
        'LinkPages' => 'Text',
132
        'Link' => 'Text',
133
        'ListViewForm' => 'HTMLFragment',
134
        'ExtraTreeTools' => 'HTMLFragment',
135
        'PageList' => 'HTMLFragment',
136
        'PageListSidebar' => 'HTMLFragment',
137
        'SiteTreeHints' => 'HTMLFragment',
138
        'SecurityID' => 'Text',
139
        'SiteTreeAsUL' => 'HTMLFragment',
140
    );
141
142
    public function init()
143
    {
144
        // set reading lang
145
        if (SiteTree::has_extension('Translatable') && !$this->getRequest()->isAjax()) {
146
            Translatable::choose_site_locale(array_keys(Translatable::get_existing_content_languages('SilverStripe\\CMS\\Model\\SiteTree')));
147
        }
148
149
        parent::init();
150
151
        Requirements::javascript(CMS_DIR . '/client/dist/js/bundle.js');
152
        Requirements::javascript(CMS_DIR . '/client/dist/js/SilverStripeNavigator.js');
153
        Requirements::css(CMS_DIR . '/client/dist/styles/bundle.css');
154
        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...
155
        Requirements::add_i18n_javascript(CMS_DIR . '/client/lang', false, true);
156
157
        CMSBatchActionHandler::register('restore', CMSBatchAction_Restore::class);
158
        CMSBatchActionHandler::register('archive', CMSBatchAction_Archive::class);
159
        CMSBatchActionHandler::register('unpublish', CMSBatchAction_Unpublish::class);
160
        CMSBatchActionHandler::register('publish', CMSBatchAction_Publish::class);
161
    }
162
163
    public function index($request)
164
    {
165
        // In case we're not showing a specific record, explicitly remove any session state,
166
        // to avoid it being highlighted in the tree, and causing an edit form to show.
167
        if (!$request->param('Action')) {
168
            $this->setCurrentPageID(null);
169
        }
170
171
        return parent::index($request);
172
    }
173
174
    public function getResponseNegotiator()
175
    {
176
        $negotiator = parent::getResponseNegotiator();
177
178
        // ListViewForm
179
        $negotiator->setCallback('ListViewForm', function () {
180
            return $this->ListViewForm()->forTemplate();
181
        });
182
183
        // PageList view
184
        $negotiator->setCallback('Content-PageList', function () {
185
            return $this->PageList()->forTemplate();
186
        });
187
188
        // PageList view for edit controller
189
        $negotiator->setCallback('Content-PageList-Sidebar', function () {
190
            return $this->PageListSidebar()->forTemplate();
191
        });
192
193
        return $negotiator;
194
    }
195
196
    /**
197
     * Get pages listing area
198
     *
199
     * @return DBHTMLText
200
     */
201
    public function PageList()
202
    {
203
        return $this->renderWith($this->getTemplatesWithSuffix('_PageList'));
204
    }
205
206
    /**
207
     * Page list view for edit-form
208
     *
209
     * @return DBHTMLText
210
     */
211
    public function PageListSidebar()
212
    {
213
        return $this->renderWith($this->getTemplatesWithSuffix('_PageList_Sidebar'));
214
    }
215
216
    /**
217
     * If this is set to true, the "switchView" context in the
218
     * template is shown, with links to the staging and publish site.
219
     *
220
     * @return boolean
221
     */
222
    public function ShowSwitchView()
223
    {
224
        return true;
225
    }
226
227
    /**
228
     * Overloads the LeftAndMain::ShowView. Allows to pass a page as a parameter, so we are able
229
     * to switch view also for archived versions.
230
     *
231
     * @param SiteTree $page
232
     * @return array
233
     */
234
    public function SwitchView($page = null)
235
    {
236
        if (!$page) {
237
            $page = $this->currentPage();
238
        }
239
240
        if ($page) {
241
            $nav = SilverStripeNavigator::get_for_record($page);
242
            return $nav['items'];
243
        }
244
    }
245
246
    //------------------------------------------------------------------------------------------//
247
    // Main controllers
248
249
    //------------------------------------------------------------------------------------------//
250
    // Main UI components
251
252
    /**
253
     * Override {@link LeftAndMain} Link to allow blank URL segment for CMSMain.
254
     *
255
     * @param string|null $action Action to link to.
256
     * @return string
257
     */
258
    public function Link($action = null)
259
    {
260
        $link = Controller::join_links(
261
            AdminRootController::admin_url(),
262
            $this->stat('url_segment'), // in case we want to change the segment
263
            '/', // trailing slash needed if $action is null!
264
            "$action"
265
        );
266
        $this->extend('updateLink', $link);
267
        return $link;
268
    }
269
270
    public function LinkPages()
271
    {
272
        return CMSPagesController::singleton()->Link();
273
    }
274
275
    public function LinkPagesWithSearch()
276
    {
277
        return $this->LinkWithSearch($this->LinkPages());
278
    }
279
280
    /**
281
     * Get link to tree view
282
     *
283
     * @return string
284
     */
285
    public function LinkTreeView()
286
    {
287
        // Tree view is just default link to main pages section (no /treeview suffix)
288
        return $this->LinkWithSearch(CMSMain::singleton()->Link());
289
    }
290
291
    /**
292
     * Get link to list view
293
     *
294
     * @return string
295
     */
296
    public function LinkListView()
297
    {
298
        // Note : Force redirect to top level page controller
299
        return $this->LinkWithSearch(CMSMain::singleton()->Link('listview'));
300
    }
301
302 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...
303
    {
304
        if (!$id) {
305
            $id = $this->currentPageID();
306
        }
307
        return $this->LinkWithSearch(
308
            Controller::join_links(CMSPageEditController::singleton()->Link('show'), $id)
309
        );
310
    }
311
312 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...
313
    {
314
        if ($id = $this->currentPageID()) {
315
            return $this->LinkWithSearch(
316
                Controller::join_links(CMSPageSettingsController::singleton()->Link('show'), $id)
317
            );
318
        } else {
319
            return null;
320
        }
321
    }
322
323 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...
324
    {
325
        if ($id = $this->currentPageID()) {
326
            return $this->LinkWithSearch(
327
                Controller::join_links(CMSPageHistoryController::singleton()->Link('show'), $id)
328
            );
329
        } else {
330
            return null;
331
        }
332
    }
333
334
    public function LinkWithSearch($link)
335
    {
336
        // Whitelist to avoid side effects
337
        $params = array(
338
            'q' => (array)$this->getRequest()->getVar('q'),
339
            'ParentID' => $this->getRequest()->getVar('ParentID')
340
        );
341
        $link = Controller::join_links(
342
            $link,
343
            array_filter(array_values($params)) ? '?' . http_build_query($params) : null
344
        );
345
        $this->extend('updateLinkWithSearch', $link);
346
        return $link;
347
    }
348
349
    public function LinkPageAdd($extra = null, $placeholders = null)
350
    {
351
        $link = CMSPageAddController::singleton()->Link();
352
        $this->extend('updateLinkPageAdd', $link);
353
354
        if ($extra) {
355
            $link = Controller::join_links($link, $extra);
356
        }
357
358
        if ($placeholders) {
359
            $link .= (strpos($link, '?') === false ? "?$placeholders" : "&amp;$placeholders");
360
        }
361
362
        return $link;
363
    }
364
365
    /**
366
     * @return string
367
     */
368
    public function LinkPreview()
369
    {
370
        $record = $this->getRecord($this->currentPageID());
371
        $baseLink = Director::absoluteBaseURL();
372
        if ($record && $record instanceof Page) {
0 ignored issues
show
Bug introduced by
The class Page does not exist. Is this class maybe located in a folder that is not analyzed, or in a newer version of your dependencies than listed in your composer.lock/composer.json?
Loading history...
373
            // if we are an external redirector don't show a link
374
            if ($record instanceof RedirectorPage && $record->RedirectionType == 'External') {
375
                $baseLink = false;
376
            } else {
377
                $baseLink = $record->Link('?stage=Stage');
378
            }
379
        }
380
        return $baseLink;
381
    }
382
383
    /**
384
     * Return the entire site tree as a nested set of ULs
385
     */
386
    public function SiteTreeAsUL()
387
    {
388
        // Pre-cache sitetree version numbers for querying efficiency
389
        Versioned::prepopulate_versionnumber_cache(SiteTree::class, "Stage");
390
        Versioned::prepopulate_versionnumber_cache(SiteTree::class, "Live");
391
        $html = $this->getSiteTreeFor($this->stat('tree_class'));
392
393
        $this->extend('updateSiteTreeAsUL', $html);
394
395
        return $html;
396
    }
397
398
    /**
399
     * @return boolean
400
     */
401
    public function TreeIsFiltered()
402
    {
403
        $query = $this->getRequest()->getVar('q');
404
405
        if (!$query || (count($query) === 1 && isset($query['FilterClass']) && $query['FilterClass'] === 'SilverStripe\\CMS\\Controllers\\CMSSiteTreeFilter_Search')) {
406
            return false;
407
        }
408
409
        return true;
410
    }
411
412
    public function ExtraTreeTools()
413
    {
414
        $html = '';
415
        $this->extend('updateExtraTreeTools', $html);
416
        return $html;
417
    }
418
419
    /**
420
     * Returns a Form for page searching for use in templates.
421
     *
422
     * Can be modified from a decorator by a 'updateSearchForm' method
423
     *
424
     * @return Form
425
     */
426
    public function SearchForm()
427
    {
428
        // Create the fields
429
        $content = new TextField('q[Term]', _t('CMSSearch.FILTERLABELTEXT', 'Search'));
430
        $dateFrom = new DateField(
431
            'q[LastEditedFrom]',
432
            _t('CMSSearch.FILTERDATEFROM', 'From')
433
        );
434
        $dateFrom->setConfig('showcalendar', true);
435
        $dateTo = new DateField(
436
            'q[LastEditedTo]',
437
            _t('CMSSearch.FILTERDATETO', 'To')
438
        );
439
        $dateTo->setConfig('showcalendar', true);
440
        $pageFilter = new DropdownField(
441
            'q[FilterClass]',
442
            _t('CMSMain.PAGES', 'Page status'),
443
            CMSSiteTreeFilter::get_all_filters()
444
        );
445
        $pageClasses = new DropdownField(
446
            'q[ClassName]',
447
            _t('CMSMain.PAGETYPEOPT', 'Page type', 'Dropdown for limiting search to a page type'),
448
            $this->getPageTypes()
449
        );
450
        $pageClasses->setEmptyString(_t('CMSMain.PAGETYPEANYOPT', 'Any'));
451
452
        // Group the Datefields
453
        $dateGroup = new FieldGroup(
454
            $dateFrom,
455
            $dateTo
456
        );
457
        $dateGroup->setTitle(_t('CMSSearch.PAGEFILTERDATEHEADING', 'Last edited'));
458
459
        // view mode
460
        $viewMode = HiddenField::create('view', false, $this->ViewState());
461
462
        // Create the Field list
463
        $fields = new FieldList(
464
            $content,
465
            $pageFilter,
466
            $pageClasses,
467
            $dateGroup,
468
            $viewMode
469
        );
470
471
        // Create the Search and Reset action
472
        $actions = new FieldList(
473
            FormAction::create('doSearch', _t('CMSMain_left_ss.APPLY_FILTER', 'Search'))
474
                ->addExtraClass('ss-ui-action-constructive'),
475
            ResetFormAction::create('clear', _t('CMSMain_left_ss.CLEAR_FILTER', 'Clear'))
476
        );
477
478
        // Use <button> to allow full jQuery UI styling on the all of the Actions
479
        /** @var FormAction $action */
480
        foreach ($actions->dataFields() as $action) {
481
            /** @var FormAction $action */
482
            $action->setUseButtonTag(true);
483
        }
484
485
        // Create the form
486
        /** @skipUpgrade */
487
        $form = Form::create($this, 'SearchForm', $fields, $actions)
488
            ->addExtraClass('cms-search-form')
489
            ->setFormMethod('GET')
490
            ->setFormAction($this->Link())
491
            ->disableSecurityToken()
492
            ->unsetValidator();
493
494
        // Load the form with previously sent search data
495
        $form->loadDataFrom($this->getRequest()->getVars());
496
497
        // Allow decorators to modify the form
498
        $this->extend('updateSearchForm', $form);
499
500
        return $form;
501
    }
502
503
    /**
504
     * Returns a sorted array suitable for a dropdown with pagetypes and their translated name
505
     *
506
     * @return array
507
     */
508
    protected function getPageTypes()
509
    {
510
        $pageTypes = array();
511
        foreach (SiteTree::page_type_classes() as $pageTypeClass) {
512
            $pageTypes[$pageTypeClass] = SiteTree::singleton($pageTypeClass)->i18n_singular_name();
513
        }
514
        asort($pageTypes);
515
        return $pageTypes;
516
    }
517
518
    public function doSearch($data, $form)
0 ignored issues
show
Unused Code introduced by
The parameter $data is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $form is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
519
    {
520
        return $this->getsubtree($this->getRequest());
521
    }
522
523
    /**
524
     * @param bool $unlinked
525
     * @return ArrayList
526
     */
527
    public function Breadcrumbs($unlinked = false)
528
    {
529
        $items = parent::Breadcrumbs($unlinked);
530
531
        if ($items->count() > 1) {
532
            // Specific to the SiteTree admin section, we never show the cms section and current
533
            // page in the same breadcrumbs block.
534
            $items->shift();
535
        }
536
537
        return $items;
538
    }
539
540
    /**
541
     * Create serialized JSON string with site tree hints data to be injected into
542
     * 'data-hints' attribute of root node of jsTree.
543
     *
544
     * @return string Serialized JSON
545
     */
546
    public function SiteTreeHints()
547
    {
548
        $classes = SiteTree::page_type_classes();
549
550
        $cacheCanCreate = array();
551
        foreach ($classes as $class) {
552
            $cacheCanCreate[$class] = singleton($class)->canCreate();
553
        }
554
555
        // Generate basic cache key. Too complex to encompass all variations
556
        $cache = Cache::factory('CMSMain_SiteTreeHints');
557
        $cacheKey = md5(implode('_', array(Member::currentUserID(), implode(',', $cacheCanCreate), implode(',', $classes))));
558
        if ($this->getRequest()->getVar('flush')) {
559
            $cache->clean(Zend_Cache::CLEANING_MODE_ALL);
560
        }
561
        $json = $cache->load($cacheKey);
562
        if (!$json) {
563
            $def['Root'] = array();
0 ignored issues
show
Coding Style Comprehensibility introduced by
$def was never initialized. Although not strictly required by PHP, it is generally a good practice to add $def = array(); before regardless.

Adding an explicit array definition is generally preferable to implicit array definition as it guarantees a stable state of the code.

Let’s take a look at an example:

foreach ($collection as $item) {
    $myArray['foo'] = $item->getFoo();

    if ($item->hasBar()) {
        $myArray['bar'] = $item->getBar();
    }

    // do something with $myArray
}

As you can see in this example, the array $myArray is initialized the first time when the foreach loop is entered. You can also see that the value of the bar key is only written conditionally; thus, its value might result from a previous iteration.

This might or might not be intended. To make your intention clear, your code more readible and to avoid accidental bugs, we recommend to add an explicit initialization $myArray = array() either outside or inside the foreach loop.

Loading history...
564
            $def['Root']['disallowedChildren'] = array();
565
566
            // Contains all possible classes to support UI controls listing them all,
567
            // such as the "add page here" context menu.
568
            $def['All'] = array();
569
570
            // Identify disallows and set globals
571
            foreach ($classes as $class) {
572
                $obj = singleton($class);
573
                if ($obj instanceof HiddenClass) {
574
                    continue;
575
                }
576
577
                // Name item
578
                $def['All'][$class] = array(
579
                    'title' => $obj->i18n_singular_name()
580
                );
581
582
                // Check if can be created at the root
583
                $needsPerm = $obj->stat('need_permission');
584
                if (!$obj->stat('can_be_root')
585
                    || (!array_key_exists($class, $cacheCanCreate) || !$cacheCanCreate[$class])
586
                    || ($needsPerm && !$this->can($needsPerm))
587
                ) {
588
                    $def['Root']['disallowedChildren'][] = $class;
589
                }
590
591
                // Hint data specific to the class
592
                $def[$class] = array();
593
594
                $defaultChild = $obj->defaultChild();
595
                if ($defaultChild !== 'Page' && $defaultChild !== null) {
596
                    $def[$class]['defaultChild'] = $defaultChild;
597
                }
598
599
                $defaultParent = $obj->defaultParent();
600
                if ($defaultParent !== 1 && $defaultParent !== null) {
601
                    $def[$class]['defaultParent'] = $defaultParent;
602
                }
603
            }
604
605
            $this->extend('updateSiteTreeHints', $def);
606
607
            $json = Convert::raw2json($def);
608
            $cache->save($json, $cacheKey);
609
        }
610
        return $json;
611
    }
612
613
    /**
614
     * Populates an array of classes in the CMS
615
     * which allows the user to change the page type.
616
     *
617
     * @return SS_List
618
     */
619
    public function PageTypes()
620
    {
621
        $classes = SiteTree::page_type_classes();
622
623
        $result = new ArrayList();
624
625
        foreach ($classes as $class) {
626
            $instance = singleton($class);
627
628
            if ($instance instanceof HiddenClass) {
629
                continue;
630
            }
631
632
            // skip this type if it is restricted
633
            if ($instance->stat('need_permission') && !$this->can(singleton($class)->stat('need_permission'))) {
634
                continue;
635
            }
636
637
            $addAction = $instance->i18n_singular_name();
638
639
            // Get description (convert 'Page' to 'SiteTree' for correct localization lookups)
640
            $i18nClass = ($class == 'Page') ? 'SilverStripe\\CMS\\Model\\SiteTree' : $class;
641
            $description = _t($i18nClass . '.DESCRIPTION');
642
643
            if (!$description) {
644
                $description = $instance->uninherited('description');
645
            }
646
647
            if ($class == 'Page' && !$description) {
648
                $description = SiteTree::singleton()->uninherited('description');
649
            }
650
651
            $result->push(new ArrayData(array(
652
                'ClassName' => $class,
653
                'AddAction' => $addAction,
654
                'Description' => $description,
655
                // TODO Sprite support
656
                'IconURL' => $instance->stat('icon'),
657
                'Title' => singleton($class)->i18n_singular_name(),
658
            )));
659
        }
660
661
        $result = $result->sort('AddAction');
662
663
        return $result;
664
    }
665
666
    /**
667
     * Get a database record to be managed by the CMS.
668
     *
669
     * @param int $id Record ID
670
     * @param int $versionID optional Version id of the given record
671
     * @return SiteTree
672
     */
673
    public function getRecord($id, $versionID = null)
674
    {
675
        $treeClass = $this->stat('tree_class');
676
677
        if ($id instanceof $treeClass) {
678
            return $id;
679
        } elseif ($id && is_numeric($id)) {
680
            $currentStage = Versioned::get_reading_mode();
681
682
            if ($this->getRequest()->getVar('Version')) {
683
                $versionID = (int) $this->getRequest()->getVar('Version');
684
            }
685
686
            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...
687
                $record = Versioned::get_version($treeClass, $id, $versionID);
688
            } else {
689
                $record = DataObject::get_by_id($treeClass, $id);
690
            }
691
692
            // Then, try getting a record from the live site
693
            if (!$record) {
694
                // $record = Versioned::get_one_by_stage($treeClass, "Live", "\"$treeClass\".\"ID\" = $id");
695
                Versioned::set_stage(Versioned::LIVE);
696
                singleton($treeClass)->flushCache();
697
698
                $record = DataObject::get_by_id($treeClass, $id);
699
            }
700
701
            // Then, try getting a deleted record
702
            if (!$record) {
703
                $record = Versioned::get_latest_version($treeClass, $id);
704
            }
705
706
            // Don't open a page from a different locale
707
            /** The record's Locale is saved in database in 2.4, and not related with Session,
708
             *  we should not check their locale matches the Translatable::get_current_locale,
709
             *  here as long as we all the HTTPRequest is init with right locale.
710
             *  This bit breaks the all FileIFrameField functions if the field is used in CMS
711
             *  and its relevent ajax calles, like loading the tree dropdown for TreeSelectorField.
712
             */
713
            /* if($record && SiteTree::has_extension('Translatable') && $record->Locale && $record->Locale != Translatable::get_current_locale()) {
714
				$record = null;
715
			}*/
716
717
            // Set the reading mode back to what it was.
718
            Versioned::set_reading_mode($currentStage);
719
720
            return $record;
721
        } elseif (substr($id, 0, 3) == 'new') {
722
            return $this->getNewItem($id);
723
        }
724
    }
725
726
    /**
727
     * @param int $id
728
     * @param FieldList $fields
729
     * @return Form
730
     */
731
    public function getEditForm($id = null, $fields = null)
732
    {
733
        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...
734
            $id = $this->currentPageID();
735
        }
736
        $form = parent::getEditForm($id, $fields);
737
738
        // TODO Duplicate record fetching (see parent implementation)
739
        $record = $this->getRecord($id);
740
        if ($record && !$record->canView()) {
741
            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 741 which is incompatible with the return type documented by SilverStripe\CMS\Controllers\CMSMain::getEditForm of type SilverStripe\Forms\Form.
Loading history...
742
        }
743
744
        if (!$fields) {
745
            $fields = $form->Fields();
746
        }
747
        $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...
748
749
        if ($record) {
750
            $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...
751
752
            $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...
753
            // Necessary for different subsites
754
            $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...
755
            $fields->push($liveLinkField = new HiddenField("LiveLink"));
756
            $fields->push($stageLinkField = new HiddenField("StageLink"));
757
            $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...
758
759
            if ($record->ID && is_numeric($record->ID)) {
760
                $liveLink = $record->getAbsoluteLiveLink();
761
                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...
762
                    $liveLinkField->setValue($liveLink);
763
                }
764
                if (!$deletedFromStage) {
765
                    $stageLink = Controller::join_links($record->AbsoluteLink(), '?stage=Stage');
766
                    if ($stageLink) {
767
                        $stageLinkField->setValue($stageLink);
768
                    }
769
                }
770
            }
771
772
            // Added in-line to the form, but plucked into different view by LeftAndMain.Preview.js upon load
773
            /** @skipUpgrade */
774
            if ($record instanceof CMSPreviewable && !$fields->fieldByName('SilverStripeNavigator')) {
775
                $navField = new LiteralField('SilverStripeNavigator', $this->getSilverStripeNavigator());
776
                $navField->setAllowHTML(true);
777
                $fields->push($navField);
778
            }
779
780
            // getAllCMSActions can be used to completely redefine the action list
781
            if ($record->hasMethod('getAllCMSActions')) {
782
                $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...
783
            } else {
784
                $actions = $record->getCMSActions();
785
786
                // Find and remove action menus that have no actions.
787
                if ($actions && $actions->count()) {
788
                    /** @var TabSet $tabset */
789
                    $tabset = $actions->fieldByName('ActionMenus');
790
                    if ($tabset) {
791
                        foreach ($tabset->getChildren() as $tab) {
792
                            if (!$tab->getChildren()->count()) {
793
                                $tabset->removeByName($tab->getName());
794
                            }
795
                        }
796
                    }
797
                }
798
            }
799
800
            // Use <button> to allow full jQuery UI styling
801
            $actionsFlattened = $actions->dataFields();
802
            if ($actionsFlattened) {
803
                /** @var FormAction $action */
804
                foreach ($actionsFlattened as $action) {
805
                    $action->setUseButtonTag(true);
806
                }
807
            }
808
809
            if ($record->hasMethod('getCMSValidator')) {
810
                $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...
811
            } else {
812
                $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...
813
            }
814
815
            // TODO Can't merge $FormAttributes in template at the moment
816
            $form->addExtraClass('center ' . $this->BaseCSSClasses());
817
            // Set validation exemptions for specific actions
818
            $form->setValidationExemptActions(array('restore', 'revert', 'deletefromlive', 'delete', 'unpublish', 'rollback', 'doRollback'));
819
820
            // Announce the capability so the frontend can decide whether to allow preview or not.
821
            if ($record instanceof CMSPreviewable) {
822
                $form->addExtraClass('cms-previewable');
823
            }
824
            $form->addExtraClass('fill-height flexbox-area-grow');
825
826
            if (!$record->canEdit() || $deletedFromStage) {
827
                $readonlyFields = $form->Fields()->makeReadonly();
828
                $form->setFields($readonlyFields);
829
            }
830
831
            $form->Fields()->setForm($form);
832
833
            $this->extend('updateEditForm', $form);
834
            return $form;
835
        } elseif ($id) {
836
            $form = Form::create($this, "EditForm", new FieldList(
837
                new LabelField('PageDoesntExistLabel', _t('CMSMain.PAGENOTEXISTS', "This page doesn't exist"))
838
            ), new FieldList())->setHTMLID('Form_EditForm');
839
            return $form;
840
        }
841
    }
842
843
    /**
844
     * @param HTTPRequest $request
845
     * @return string HTML
846
     */
847
    public function treeview($request)
848
    {
849
        return $this->getResponseNegotiator()->respond($request);
850
    }
851
852
    /**
853
     * @param HTTPRequest $request
854
     * @return string HTML
855
     */
856
    public function listview($request)
857
    {
858
        return $this->getResponseNegotiator()->respond($request);
859
    }
860
861
    /**
862
     * @return string
863
     */
864
    public function ViewState()
865
    {
866
        $mode = $this->getRequest()->requestVar('view')
867
            ?: $this->getRequest()->param('Action');
868
        switch ($mode) {
869
            case 'listview':
870
            case 'treeview':
871
                return $mode;
872
            default:
873
                return 'treeview';
874
        }
875
    }
876
877
    /**
878
     * Callback to request the list of page types allowed under a given page instance.
879
     * Provides a slower but more precise response over SiteTreeHints
880
     *
881
     * @param HTTPRequest $request
882
     * @return HTTPResponse
883
     */
884
    public function childfilter($request)
885
    {
886
        // Check valid parent specified
887
        $parentID = $request->requestVar('ParentID');
888
        $parent = SiteTree::get()->byID($parentID);
889
        if (!$parent || !$parent->exists()) {
890
            return $this->httpError(404);
891
        }
892
893
        // Build hints specific to this class
894
        // Identify disallows and set globals
895
        $classes = SiteTree::page_type_classes();
896
        $disallowedChildren = array();
897
        foreach ($classes as $class) {
898
            $obj = singleton($class);
899
            if ($obj instanceof HiddenClass) {
900
                continue;
901
            }
902
903
            if (!$obj->canCreate(null, array('Parent' => $parent))) {
904
                $disallowedChildren[] = $class;
905
            }
906
        }
907
908
        $this->extend('updateChildFilter', $disallowedChildren, $parentID);
909
        return $this
910
            ->getResponse()
911
            ->addHeader('Content-Type', 'application/json; charset=utf-8')
912
            ->setBody(Convert::raw2json($disallowedChildren));
913
    }
914
915
    /**
916
     * Safely reconstruct a selected filter from a given set of query parameters
917
     *
918
     * @param array $params Query parameters to use
919
     * @return CMSSiteTreeFilter The filter class, or null if none present
920
     * @throws InvalidArgumentException if invalid filter class is passed.
921
     */
922
    protected function getQueryFilter($params)
923
    {
924
        if (empty($params['FilterClass'])) {
925
            return null;
926
        }
927
        $filterClass = $params['FilterClass'];
928
        if (!is_subclass_of($filterClass, 'SilverStripe\\CMS\\Controllers\\CMSSiteTreeFilter')) {
929
            throw new InvalidArgumentException("Invalid filter class passed: {$filterClass}");
930
        }
931
        return $filterClass::create($params);
932
    }
933
934
    /**
935
     * Returns the pages meet a certain criteria as {@see CMSSiteTreeFilter} or the subpages of a parent page
936
     * defaulting to no filter and show all pages in first level.
937
     * Doubles as search results, if any search parameters are set through {@link SearchForm()}.
938
     *
939
     * @param array $params Search filter criteria
940
     * @param int $parentID Optional parent node to filter on (can't be combined with other search criteria)
941
     * @return SS_List
942
     * @throws InvalidArgumentException if invalid filter class is passed.
943
     */
944
    public function getList($params = array(), $parentID = 0)
945
    {
946
        if ($filter = $this->getQueryFilter($params)) {
947
            return $filter->getFilteredPages();
948
        } else {
949
            $list = DataList::create($this->stat('tree_class'));
950
            $parentID = is_numeric($parentID) ? $parentID : 0;
951
            return $list->filter("ParentID", $parentID);
952
        }
953
    }
954
955
    /**
956
     * @return Form
957
     */
958
    public function ListViewForm()
959
    {
960
        $params = $this->getRequest()->requestVar('q');
961
        $list = $this->getList($params, $parentID = $this->getRequest()->requestVar('ParentID'));
962
        $gridFieldConfig = GridFieldConfig::create()->addComponents(
963
            new GridFieldSortableHeader(),
964
            new GridFieldDataColumns(),
965
            new GridFieldPaginator(self::config()->page_length)
966
        );
967
        if ($parentID) {
968
            $linkSpec = $this->Link();
969
            $linkSpec = $linkSpec . (strstr($linkSpec, '?') ? '&' : '?') . 'ParentID=%d&view=listview';
970
            $gridFieldConfig->addComponent(
971
                GridFieldLevelup::create($parentID)
972
                    ->setLinkSpec($linkSpec)
973
                    ->setAttributes(array('data-pjax' => 'ListViewForm,Breadcrumbs'))
974
            );
975
        }
976
        $gridField = new GridField('Page', 'Pages', $list, $gridFieldConfig);
977
        /** @var GridFieldDataColumns $columns */
978
        $columns = $gridField->getConfig()->getComponentByType('SilverStripe\\Forms\\GridField\\GridFieldDataColumns');
979
980
        // Don't allow navigating into children nodes on filtered lists
981
        $fields = array(
982
            'getTreeTitle' => _t('SiteTree.PAGETITLE', 'Page Title'),
983
            'singular_name' => _t('SiteTree.PAGETYPE'),
984
            'LastEdited' => _t('SiteTree.LASTUPDATED', 'Last Updated'),
985
        );
986
        /** @var GridFieldSortableHeader $sortableHeader */
987
        $sortableHeader = $gridField->getConfig()->getComponentByType('SilverStripe\\Forms\\GridField\\GridFieldSortableHeader');
988
        $sortableHeader->setFieldSorting(array('getTreeTitle' => 'Title'));
989
        $gridField->getState()->ParentID = $parentID;
990
991
        if (!$params) {
992
            $fields = array_merge(array('listChildrenLink' => ''), $fields);
993
        }
994
995
        $columns->setDisplayFields($fields);
996
        $columns->setFieldCasting(array(
997
            'Created' => 'DBDatetime->Ago',
998
            'LastEdited' => 'DBDatetime->FormatFromSettings',
999
            'getTreeTitle' => 'HTMLFragment'
1000
        ));
1001
1002
        $controller = $this;
1003
        $columns->setFieldFormatting(array(
1004
            'listChildrenLink' => function ($value, &$item) use ($controller) {
1005
                /** @var SiteTree $item */
1006
                $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...
1007
                if ($num) {
1008
                    return sprintf(
1009
                        '<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>',
1010
                        Controller::join_links(
1011
                            $controller->Link(),
1012
                            sprintf("?ParentID=%d&view=listview", (int)$item->ID)
1013
                        ),
1014
                        $num
1015
                    );
1016
                }
1017
            },
1018
            'getTreeTitle' => function ($value, &$item) use ($controller) {
1019
                return sprintf(
1020
                    '<a class="action-detail" href="%s">%s</a>',
1021
                    Controller::join_links(
1022
                        CMSPageEditController::singleton()->Link('show'),
1023
                        (int)$item->ID
1024
                    ),
1025
                    $item->TreeTitle // returns HTML, does its own escaping
1026
                );
1027
            }
1028
        ));
1029
1030
        $negotiator = $this->getResponseNegotiator();
1031
        $listview = Form::create(
1032
            $this,
1033
            'ListViewForm',
1034
            new FieldList($gridField),
1035
            new FieldList()
1036
        )->setHTMLID('Form_ListViewForm');
1037
        $listview->setAttribute('data-pjax-fragment', 'ListViewForm');
1038 View Code Duplication
        $listview->setValidationResponseCallback(function () use ($negotiator, $listview) {
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...
1039
            $request = $this->getRequest();
1040
            if ($request->isAjax() && $negotiator) {
1041
                $listview->setupFormErrors();
1042
                $result = $listview->forTemplate();
1043
1044
                return $negotiator->respond($request, array(
1045
                    'CurrentForm' => function () use ($result) {
1046
                        return $result;
1047
                    }
1048
                ));
1049
            }
1050
        });
1051
1052
        $this->extend('updateListView', $listview);
1053
1054
        $listview->disableSecurityToken();
1055
        return $listview;
1056
    }
1057
1058
    public function currentPageID()
1059
    {
1060
        $id = parent::currentPageID();
1061
1062
        $this->extend('updateCurrentPageID', $id);
1063
1064
        return $id;
1065
    }
1066
1067
    //------------------------------------------------------------------------------------------//
1068
    // Data saving handlers
1069
1070
    /**
1071
     * Save and Publish page handler
1072
     *
1073
     * @param array $data
1074
     * @param Form $form
1075
     * @return HTTPResponse
1076
     * @throws HTTPResponse_Exception
1077
     */
1078
    public function save($data, $form)
1079
    {
1080
        $className = $this->stat('tree_class');
1081
1082
        // Existing or new record?
1083
        $id = $data['ID'];
1084
        if (substr($id, 0, 3) != 'new') {
1085
            /** @var SiteTree $record */
1086
            $record = DataObject::get_by_id($className, $id);
1087
            // Check edit permissions
1088
            if ($record && !$record->canEdit()) {
1089
                return Security::permissionFailure($this);
1090
            }
1091
            if (!$record || !$record->ID) {
1092
                throw new HTTPResponse_Exception("Bad record ID #$id", 404);
1093
            }
1094
        } else {
1095
            if (!$className::singleton()->canCreate()) {
1096
                return Security::permissionFailure($this);
1097
            }
1098
            $record = $this->getNewItem($id, false);
1099
        }
1100
1101
        // Check publishing permissions
1102
        $doPublish = !empty($data['publish']);
1103
        if ($record && $doPublish && !$record->canPublish()) {
1104
            return Security::permissionFailure($this);
1105
        }
1106
1107
        // TODO Coupling to SiteTree
1108
        $record->HasBrokenLink = 0;
1109
        $record->HasBrokenFile = 0;
1110
1111
        if (!$record->ObsoleteClassName) {
1112
            $record->writeWithoutVersion();
1113
        }
1114
1115
        // Update the class instance if necessary
1116
        if (isset($data['ClassName']) && $data['ClassName'] != $record->ClassName) {
1117
            // Replace $record with a new instance of the new class
1118
            $newClassName = $data['ClassName'];
1119
            $record = $record->newClassInstance($newClassName);
1120
        }
1121
1122
        // save form data into record
1123
        $form->saveInto($record);
1124
        $record->write();
1125
1126
        // If the 'Save & Publish' button was clicked, also publish the page
1127
        if ($doPublish) {
1128
            $record->publishRecursive();
1129
            $message = _t(
1130
                'CMSMain.PUBLISHED',
1131
                "Published '{title}' successfully.",
1132
                ['title' => $record->Title]
0 ignored issues
show
Documentation introduced by
array('title' => $record->Title) is of type array<string,?,{"title":"?"}>, but the function expects a string.

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

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

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1139
            );
1140
        }
1141
1142
        $this->getResponse()->addHeader('X-Status', rawurlencode($message));
1143
        return $this->getResponseNegotiator()->respond($this->getRequest());
1144
    }
1145
1146
    /**
1147
     * @uses LeftAndMainExtension->augmentNewSiteTreeItem()
1148
     *
1149
     * @param int|string $id
1150
     * @param bool $setID
1151
     * @return mixed|DataObject
1152
     * @throws HTTPResponse_Exception
1153
     */
1154
    public function getNewItem($id, $setID = true)
1155
    {
1156
        $parentClass = $this->stat('tree_class');
1157
        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...
1158
1159
        if (!is_a($className, $parentClass, true)) {
1160
            $response = Security::permissionFailure($this);
1161
            if (!$response) {
1162
                $response = $this->getResponse();
1163
            }
1164
            throw new HTTPResponse_Exception($response);
1165
        }
1166
1167
        /** @var SiteTree $newItem */
1168
        $newItem = Injector::inst()->create($className);
1169
        if (!$suffix) {
1170
            $sessionTag = "NewItems." . $parentID . "." . $className;
1171
            if (Session::get($sessionTag)) {
1172
                $suffix = '-' . Session::get($sessionTag);
1173
                Session::set($sessionTag, Session::get($sessionTag) + 1);
1174
            } else {
1175
                Session::set($sessionTag, 1);
1176
            }
1177
1178
                $id = $id . $suffix;
1179
        }
1180
1181
        $newItem->Title = _t(
1182
            'CMSMain.NEWPAGE',
1183
            "New {pagetype}",
1184
            'followed by a page type title',
1185
            array('pagetype' => singleton($className)->i18n_singular_name())
1186
        );
1187
        $newItem->ClassName = $className;
1188
        $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...
1189
1190
        // DataObject::fieldExists only checks the current class, not the hierarchy
1191
        // This allows the CMS to set the correct sort value
1192
        if ($newItem->castingHelper('Sort')) {
1193
            $newItem->Sort = DB::prepared_query('SELECT MAX("Sort") FROM "SiteTree" WHERE "ParentID" = ?', array($parentID))->value() + 1;
1194
        }
1195
1196
        if ($setID) {
1197
            $newItem->ID = $id;
1198
        }
1199
1200
        # Some modules like subsites add extra fields that need to be set when the new item is created
1201
        $this->extend('augmentNewSiteTreeItem', $newItem);
1202
1203
        return $newItem;
1204
    }
1205
1206
    /**
1207
     * Actually perform the publication step
1208
     *
1209
     * @param Versioned|DataObject $record
1210
     * @return mixed
1211
     */
1212
    public function performPublish($record)
1213
    {
1214
        if ($record && !$record->canPublish()) {
0 ignored issues
show
Bug introduced by
The method canPublish does only exist in SilverStripe\ORM\Versioning\Versioned, but not in SilverStripe\ORM\DataObject.

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

Let’s take a look at an example:

class A
{
    public function foo() { }
}

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

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

Available Fixes

  1. Add an additional type-check:

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

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

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

Let’s take a look at an example:

class A
{
    public function foo() { }
}

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

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

Available Fixes

  1. Add an additional type-check:

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

    function someFunction(B $x) { /** ... */ }
    
Loading history...
1219
    }
1220
1221
    /**
1222
     * Reverts a page by publishing it to live.
1223
     * Use {@link restorepage()} if you want to restore a page
1224
     * which was deleted from draft without publishing.
1225
     *
1226
     * @uses SiteTree->doRevertToLive()
1227
     *
1228
     * @param array $data
1229
     * @param Form $form
1230
     * @return HTTPResponse
1231
     * @throws HTTPResponse_Exception
1232
     */
1233
    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...
1234
    {
1235
        if (!isset($data['ID'])) {
1236
            throw new HTTPResponse_Exception("Please pass an ID in the form content", 400);
1237
        }
1238
1239
        $id = (int) $data['ID'];
1240
        $restoredPage = Versioned::get_latest_version(SiteTree::class, $id);
1241
        if (!$restoredPage) {
1242
            throw new HTTPResponse_Exception("SiteTree #$id not found", 400);
1243
        }
1244
1245
        /** @var SiteTree $record */
1246
        $record = Versioned::get_one_by_stage('SilverStripe\\CMS\\Model\\SiteTree', 'Live', array(
0 ignored issues
show
Documentation introduced by
array('"SiteTree_Live"."ID"' => $id) is of type array<string,integer,{"\...e\".\"ID\"":"integer"}>, but the function expects a string.

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

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

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

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

This check looks for function calls that miss required arguments.

Loading history...
1397
            return Security::permissionFailure($this);
1398
        }
1399
1400
        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...
1401
            $record->doRollbackTo($version);
0 ignored issues
show
Bug introduced by
The method doRollbackTo does only exist in SilverStripe\ORM\Versioning\Versioned, but not in SilverStripe\ORM\DataObject.

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

Let’s take a look at an example:

class A
{
    public function foo() { }
}

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

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

Available Fixes

  1. Add an additional type-check:

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

    function someFunction(B $x) { /** ... */ }
    
Loading history...
1402
            $message = _t(
1403
                'CMSMain.ROLLEDBACKVERSIONv2',
1404
                "Rolled back to version #%d.",
1405
                array('version' => $data['Version'])
0 ignored issues
show
Documentation introduced by
array('version' => $data['Version']) is of type array<string,?,{"version":"?"}>, but the function expects a string.

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1406
            );
1407
        } else {
1408
            $record->doRevertToLive();
0 ignored issues
show
Bug introduced by
The method doRevertToLive does only exist in SilverStripe\ORM\Versioning\Versioned, but not in SilverStripe\ORM\DataObject.

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

Let’s take a look at an example:

class A
{
    public function foo() { }
}

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

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

Available Fixes

  1. Add an additional type-check:

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

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

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

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

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

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

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

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

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

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

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

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