Completed
Push — master ( a03f6b...75451a )
by Daniel
02:19
created

CMSMain::LinkListViewChildren()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

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

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
474
                $childrenMethod = $filter->getChildrenMethod();
475
            }
476
            if (!$numChildrenMethod) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $numChildrenMethod of type string|null is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
477
                $numChildrenMethod = $filter->getNumChildrenMethod();
478
            }
479
            if (!$filterFunction) {
480
                $filterFunction = function ($node) use ($filter) {
481
                    return $filter->isPageIncluded($node);
482
                };
483
            }
484
        }
485
486
        // Build set from node and begin marking
487
        $record = ($rootID) ? $this->getRecord($rootID) : null;
488
        $rootNode = $record ? $record : DataObject::singleton($className);
489
        $markingSet = MarkedSet::create($rootNode, $childrenMethod, $numChildrenMethod, $nodeCountThreshold);
490
491
        // Set filter function
492
        if ($filterFunction) {
493
            $markingSet->setMarkingFilterFunction($filterFunction);
494
        }
495
496
        // Mark tree from this node
497
        $markingSet->markPartialTree();
498
499
        // Ensure current page is exposed
500
        $currentPage = $this->currentPage();
501
        if ($currentPage) {
502
            $markingSet->markToExpose($currentPage);
503
        }
504
505
        // Pre-cache permissions
506
        $checker = SiteTree::getPermissionChecker();
507
        if ($checker instanceof InheritedPermissions) {
508
            $checker->prePopulatePermissionCache(
509
                InheritedPermissions::EDIT,
510
                $markingSet->markedNodeIDs()
511
            );
512
        }
513
514
        // Render using full-subtree template
515
        return $markingSet->renderChildren(
516
            [ self::class . '_SubTree', 'type' => 'Includes' ],
0 ignored issues
show
Documentation introduced by
array(self::class . '_Su..., 'type' => 'Includes') is of type array<integer|string,str...ring","type":"string"}>, but the function expects a string|null.

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
517
            $this->getTreeNodeCustomisations()
518
        );
519
    }
520
521
522
    /**
523
     * Get callback to determine template customisations for nodes
524
     *
525
     * @return callable
526
     */
527
    protected function getTreeNodeCustomisations()
528
    {
529
        $rootTitle = $this->getCMSTreeTitle();
530
        return function (SiteTree $node) use ($rootTitle) {
531
            return [
532
                'listViewLink' => $this->LinkListViewChildren($node->ID),
533
                'rootTitle' => $rootTitle,
534
                'extraClass' => $this->getTreeNodeClasses($node),
535
            ];
536
        };
537
    }
538
539
    /**
540
     * Get extra CSS classes for a page's tree node
541
     *
542
     * @param SiteTree $node
543
     * @return string
544
     */
545
    public function getTreeNodeClasses(SiteTree $node)
546
    {
547
        // Get classes from object
548
        $classes = $node->CMSTreeClasses();
549
550
        // Flag as current
551
        if ($this->isCurrentPage($node)) {
552
            $classes .= ' current';
553
        }
554
555
        // Get status flag classes
556
        $flags = $node->getStatusFlags();
557
        if ($flags) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $flags of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
558
            $statuses = array_keys($flags);
559
            foreach ($statuses as $s) {
560
                $classes .= ' status-' . $s;
561
            }
562
        }
563
564
        // Get additional filter classes
565
        $filter = $this->getSearchFilter();
566
        if ($filter && ($filterClasses = $filter->getPageClasses($node))) {
567
            if (is_array($filterClasses)) {
568
                $filterClasses = implode(' ', $filterClasses);
569
            }
570
            $classes .= ' ' . $filterClasses;
571
        }
572
573
        return trim($classes);
574
    }
575
576
    /**
577
     * Get a subtree underneath the request param 'ID'.
578
     * If ID = 0, then get the whole tree.
579
     *
580
     * @param HTTPRequest $request
581
     * @return string
582
     */
583
    public function getsubtree($request)
584
    {
585
        $html = $this->getSiteTreeFor(
586
            $this->config()->get('tree_class'),
587
            $request->getVar('ID'),
588
            null,
589
            null,
590
            null,
591
            $request->getVar('minNodeCount')
592
        );
593
594
        // Trim off the outer tag
595
        $html = preg_replace('/^[\s\t\r\n]*<ul[^>]*>/', '', $html);
596
        $html = preg_replace('/<\/ul[^>]*>[\s\t\r\n]*$/', '', $html);
597
598
        return $html;
599
    }
600
601
    /**
602
     * Allows requesting a view update on specific tree nodes.
603
     * Similar to {@link getsubtree()}, but doesn't enforce loading
604
     * all children with the node. Useful to refresh views after
605
     * state modifications, e.g. saving a form.
606
     *
607
     * @param HTTPRequest $request
608
     * @return HTTPResponse
609
     */
610
    public function updatetreenodes($request)
611
    {
612
        $data = array();
613
        $ids = explode(',', $request->getVar('ids'));
614
        foreach ($ids as $id) {
615
            if ($id === "") {
616
                continue; // $id may be a blank string, which is invalid and should be skipped over
617
            }
618
619
            $record = $this->getRecord($id);
620
            if (!$record) {
621
                continue; // In case a page is no longer available
622
            }
623
624
            // Create marking set with sole marked root
625
            $markingSet = MarkedSet::create($record);
626
            $markingSet->setMarkingFilterFunction(function () {
627
                return false;
628
            });
629
            $markingSet->markUnexpanded($record);
630
631
            // Find the next & previous nodes, for proper positioning (Sort isn't good enough - it's not a raw offset)
632
            // TODO: These methods should really be in hierarchy - for a start it assumes Sort exists
633
            $prev = null;
634
635
            $className = $this->config()->get('tree_class');
636
            $next = DataObject::get($className)
637
                ->filter('ParentID', $record->ParentID)
638
                ->filter('Sort:GreaterThan', $record->Sort)
639
                ->first();
640
641
            if (!$next) {
642
                $prev = DataObject::get($className)
643
                    ->filter('ParentID', $record->ParentID)
644
                    ->filter('Sort:LessThan', $record->Sort)
645
                    ->reverse()
646
                    ->first();
647
            }
648
649
            // Render using single node template
650
            $html = $markingSet->renderChildren(
651
                [ self::class . '_TreeNode', 'type' => 'Includes'],
0 ignored issues
show
Documentation introduced by
array(self::class . '_Tr..., 'type' => 'Includes') is of type array<integer|string,str...ring","type":"string"}>, but the function expects a string|null.

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
652
                $this->getTreeNodeCustomisations()
653
            );
654
655
            $data[$id] = array(
656
                'html' => $html,
657
                'ParentID' => $record->ParentID,
658
                'NextID' => $next ? $next->ID : null,
659
                'PrevID' => $prev ? $prev->ID : null
660
            );
661
        }
662
        return $this
663
            ->getResponse()
664
            ->addHeader('Content-Type', 'application/json')
665
            ->setBody(Convert::raw2json($data));
666
    }
667
668
    /**
669
     * Update the position and parent of a tree node.
670
     * Only saves the node if changes were made.
671
     *
672
     * Required data:
673
     * - 'ID': The moved node
674
     * - 'ParentID': New parent relation of the moved node (0 for root)
675
     * - 'SiblingIDs': Array of all sibling nodes to the moved node (incl. the node itself).
676
     *   In case of a 'ParentID' change, relates to the new siblings under the new parent.
677
     *
678
     * @param HTTPRequest $request
679
     * @return HTTPResponse JSON string with a
680
     * @throws HTTPResponse_Exception
681
     */
682
    public function savetreenode($request)
683
    {
684
        if (!SecurityToken::inst()->checkRequest($request)) {
685
            return $this->httpError(400);
686
        }
687
        if (!Permission::check('SITETREE_REORGANISE') && !Permission::check('ADMIN')) {
688
            return $this->httpError(
689
                403,
690
                _t(
691
                    __CLASS__.'.CANT_REORGANISE',
692
                    "You do not have permission to rearange the site tree. Your change was not saved."
693
                )
694
            );
695
        }
696
697
        $className = $this->config()->get('tree_class');
698
        $id = $request->requestVar('ID');
699
        $parentID = $request->requestVar('ParentID');
700
        if (!is_numeric($id) || !is_numeric($parentID)) {
701
            return $this->httpError(400);
702
        }
703
704
        // Check record exists in the DB
705
        /** @var SiteTree $node */
706
        $node = DataObject::get_by_id($className, $id);
707
        if (!$node) {
708
            return $this->httpError(
709
                500,
710
                _t(
711
                    __CLASS__.'.PLEASESAVE',
712
                    "Please Save Page: This page could not be updated because it hasn't been saved yet."
713
                )
714
            );
715
        }
716
717
        // Check top level permissions
718
        $root = $node->getParentType();
719
        if (($parentID == '0' || $root == 'root') && !SiteConfig::current_site_config()->canCreateTopLevel()) {
720
            return $this->httpError(
721
                403,
722
                _t(
723
                    __CLASS__.'.CANT_REORGANISE',
724
                    "You do not have permission to alter Top level pages. Your change was not saved."
725
                )
726
            );
727
        }
728
729
        $siblingIDs = $request->requestVar('SiblingIDs');
730
        $statusUpdates = array('modified'=>array());
731
732
        if (!$node->canEdit()) {
733
            return Security::permissionFailure($this);
734
        }
735
736
        // Update hierarchy (only if ParentID changed)
737
        if ($node->ParentID != $parentID) {
0 ignored issues
show
Documentation introduced by
The property ParentID does not exist on object<SilverStripe\CMS\Model\SiteTree>. Since you implemented __get, maybe consider adding a @property annotation.

Since your code implements the magic getter _get, this function will be called for any read access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

If the property has read access only, you can use the @property-read annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
738
            $node->ParentID = (int)$parentID;
0 ignored issues
show
Documentation introduced by
The property ParentID does not exist on object<SilverStripe\CMS\Model\SiteTree>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
739
            $node->write();
740
741
            $statusUpdates['modified'][$node->ID] = array(
742
                'TreeTitle' => $node->TreeTitle
0 ignored issues
show
Documentation introduced by
The property TreeTitle does not exist on object<SilverStripe\CMS\Model\SiteTree>. Since you implemented __get, maybe consider adding a @property annotation.

Since your code implements the magic getter _get, this function will be called for any read access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

If the property has read access only, you can use the @property-read annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
743
            );
744
745
            // Update all dependent pages
746
            $virtualPages = VirtualPage::get()->filter("CopyContentFromID", $node->ID);
747
            foreach ($virtualPages as $virtualPage) {
748
                $statusUpdates['modified'][$virtualPage->ID] = array(
749
                    'TreeTitle' => $virtualPage->TreeTitle()
750
                );
751
            }
752
753
            $this->getResponse()->addHeader(
754
                'X-Status',
755
                rawurlencode(_t(__CLASS__.'.REORGANISATIONSUCCESSFUL', 'Reorganised the site tree successfully.'))
756
            );
757
        }
758
759
        // Update sorting
760
        if (is_array($siblingIDs)) {
761
            $counter = 0;
762
            foreach ($siblingIDs as $id) {
763
                if ($id == $node->ID) {
764
                    $node->Sort = ++$counter;
765
                    $node->write();
766
                    $statusUpdates['modified'][$node->ID] = array(
767
                        'TreeTitle' => $node->TreeTitle
0 ignored issues
show
Documentation introduced by
The property TreeTitle does not exist on object<SilverStripe\CMS\Model\SiteTree>. Since you implemented __get, maybe consider adding a @property annotation.

Since your code implements the magic getter _get, this function will be called for any read access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

If the property has read access only, you can use the @property-read annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
768
                    );
769
                } elseif (is_numeric($id)) {
770
                    // Nodes that weren't "actually moved" shouldn't be registered as
771
                    // having been edited; do a direct SQL update instead
772
                    ++$counter;
773
                    $table = DataObject::getSchema()->baseDataTable($className);
774
                    DB::prepared_query(
775
                        "UPDATE \"$table\" SET \"Sort\" = ? WHERE \"ID\" = ?",
776
                        array($counter, $id)
777
                    );
778
                }
779
            }
780
781
            $this->getResponse()->addHeader(
782
                'X-Status',
783
                rawurlencode(_t(__CLASS__.'.REORGANISATIONSUCCESSFUL', 'Reorganised the site tree successfully.'))
784
            );
785
        }
786
787
        return $this
788
            ->getResponse()
789
            ->addHeader('Content-Type', 'application/json')
790
            ->setBody(Convert::raw2json($statusUpdates));
791
    }
792
793
    public function CanOrganiseSitetree()
794
    {
795
        return !Permission::check('SITETREE_REORGANISE') && !Permission::check('ADMIN') ? false : true;
796
    }
797
798
    /**
799
     * @return boolean
800
     */
801
    public function TreeIsFiltered()
802
    {
803
        $query = $this->getRequest()->getVar('q');
804
805
        if (!$query || (count($query) === 1 && isset($query['FilterClass']) && $query['FilterClass'] === 'SilverStripe\\CMS\\Controllers\\CMSSiteTreeFilter_Search')) {
806
            return false;
807
        }
808
809
        return true;
810
    }
811
812
    public function ExtraTreeTools()
813
    {
814
        $html = '';
815
        $this->extend('updateExtraTreeTools', $html);
816
        return $html;
817
    }
818
819
    /**
820
     * Returns a Form for page searching for use in templates.
821
     *
822
     * Can be modified from a decorator by a 'updateSearchForm' method
823
     *
824
     * @return Form
825
     */
826
    public function SearchForm()
827
    {
828
        // Create the fields
829
        $content = new TextField('q[Term]', _t('SilverStripe\\CMS\\Search\\SearchForm.FILTERLABELTEXT', 'Search'));
830
        $dateFrom = new DateField(
831
            'q[LastEditedFrom]',
832
            _t('SilverStripe\\CMS\\Search\\SearchForm.FILTERDATEFROM', 'From')
833
        );
834
        $dateTo = new DateField(
835
            'q[LastEditedTo]',
836
            _t('SilverStripe\\CMS\\Search\\SearchForm.FILTERDATETO', 'To')
837
        );
838
        $pageFilter = new DropdownField(
839
            'q[FilterClass]',
840
            _t('SilverStripe\\CMS\\Controllers\\CMSMain.PAGES', 'Page status'),
841
            CMSSiteTreeFilter::get_all_filters()
842
        );
843
        $pageClasses = new DropdownField(
844
            'q[ClassName]',
845
            _t('SilverStripe\\CMS\\Controllers\\CMSMain.PAGETYPEOPT', 'Page type', 'Dropdown for limiting search to a page type'),
846
            $this->getPageTypes()
847
        );
848
        $pageClasses->setEmptyString(_t('SilverStripe\\CMS\\Controllers\\CMSMain.PAGETYPEANYOPT', 'Any'));
849
850
        // Group the Datefields
851
        $dateGroup = new FieldGroup(
852
            $dateFrom,
853
            $dateTo
854
        );
855
        $dateGroup->setTitle(_t('SilverStripe\\CMS\\Search\\SearchForm.PAGEFILTERDATEHEADING', 'Last edited'));
856
857
        // view mode
858
        $viewMode = HiddenField::create('view', false, $this->ViewState('listview'));
859
860
        // Create the Field list
861
        $fields = new FieldList(
862
            $content,
863
            $pageFilter,
864
            $pageClasses,
865
            $dateGroup,
866
            $viewMode
867
        );
868
869
        // Create the Search and Reset action
870
        $actions = new FieldList(
871
            FormAction::create('doSearch', _t('SilverStripe\\CMS\\Controllers\\CMSMain.APPLY_FILTER', 'Search'))
872
                ->addExtraClass('btn btn-primary'),
873
            FormAction::create('clear', _t('SilverStripe\\CMS\\Controllers\\CMSMain.CLEAR_FILTER', 'Clear'))
874
                ->setAttribute('type', 'reset')
875
                ->addExtraClass('btn btn-secondary')
876
        );
877
878
        // Use <button> to allow full jQuery UI styling on the all of the Actions
879
        /** @var FormAction $action */
880
        foreach ($actions->dataFields() as $action) {
881
            /** @var FormAction $action */
882
            $action->setUseButtonTag(true);
883
        }
884
885
        // Create the form
886
        /** @skipUpgrade */
887
        $form = Form::create($this, 'SearchForm', $fields, $actions)
888
            ->addExtraClass('cms-search-form')
889
            ->setFormMethod('GET')
890
            ->setFormAction(CMSMain::singleton()->Link())
891
            ->disableSecurityToken()
892
            ->unsetValidator();
893
894
        // Load the form with previously sent search data
895
        $form->loadDataFrom($this->getRequest()->getVars());
896
897
        // Allow decorators to modify the form
898
        $this->extend('updateSearchForm', $form);
899
900
        return $form;
901
    }
902
903
    /**
904
     * Returns a sorted array suitable for a dropdown with pagetypes and their translated name
905
     *
906
     * @return array
907
     */
908
    protected function getPageTypes()
909
    {
910
        $pageTypes = array();
911
        foreach (SiteTree::page_type_classes() as $pageTypeClass) {
912
            $pageTypes[$pageTypeClass] = SiteTree::singleton($pageTypeClass)->i18n_singular_name();
913
        }
914
        asort($pageTypes);
915
        return $pageTypes;
916
    }
917
918
    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...
919
    {
920
        return $this->getsubtree($this->getRequest());
921
    }
922
923
    /**
924
     * @param bool $unlinked
925
     * @return ArrayList
926
     */
927
    public function Breadcrumbs($unlinked = false)
928
    {
929
        $items = parent::Breadcrumbs($unlinked);
930
931
        if ($items->count() > 1) {
932
            // Specific to the SiteTree admin section, we never show the cms section and current
933
            // page in the same breadcrumbs block.
934
            $items->shift();
935
        }
936
937
        return $items;
938
    }
939
940
    /**
941
     * Create serialized JSON string with site tree hints data to be injected into
942
     * 'data-hints' attribute of root node of jsTree.
943
     *
944
     * @return string Serialized JSON
945
     */
946
    public function SiteTreeHints()
947
    {
948
        $classes = SiteTree::page_type_classes();
949
950
        $cacheCanCreate = array();
951
        foreach ($classes as $class) {
952
            $cacheCanCreate[$class] = singleton($class)->canCreate();
953
        }
954
955
        // Generate basic cache key. Too complex to encompass all variations
956
        $cache = Injector::inst()->get(CacheInterface::class . '.CMSMain_SiteTreeHints');
957
        $cacheKey = md5(implode('_', array(Security::getCurrentUser()->ID, implode(',', $cacheCanCreate), implode(',', $classes))));
958
        if ($this->getRequest()->getVar('flush')) {
959
            $cache->clear();
960
        }
961
        $json = $cache->get($cacheKey);
962
        if (!$json) {
963
            $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...
964
            $def['Root']['disallowedChildren'] = array();
965
966
            // Contains all possible classes to support UI controls listing them all,
967
            // such as the "add page here" context menu.
968
            $def['All'] = array();
969
970
            // Identify disallows and set globals
971
            foreach ($classes as $class) {
972
                $obj = singleton($class);
973
                if ($obj instanceof HiddenClass) {
974
                    continue;
975
                }
976
977
                // Name item
978
                $def['All'][$class] = array(
979
                    'title' => $obj->i18n_singular_name()
980
                );
981
982
                // Check if can be created at the root
983
                $needsPerm = $obj->config()->get('need_permission');
984
                if (!$obj->config()->get('can_be_root')
985
                    || (!array_key_exists($class, $cacheCanCreate) || !$cacheCanCreate[$class])
986
                    || ($needsPerm && !$this->can($needsPerm))
987
                ) {
988
                    $def['Root']['disallowedChildren'][] = $class;
989
                }
990
991
                // Hint data specific to the class
992
                $def[$class] = array();
993
994
                $defaultChild = $obj->defaultChild();
995
                if ($defaultChild !== 'Page' && $defaultChild !== null) {
996
                    $def[$class]['defaultChild'] = $defaultChild;
997
                }
998
999
                $defaultParent = $obj->defaultParent();
1000
                if ($defaultParent !== 1 && $defaultParent !== null) {
1001
                    $def[$class]['defaultParent'] = $defaultParent;
1002
                }
1003
            }
1004
1005
            $this->extend('updateSiteTreeHints', $def);
1006
1007
            $json = Convert::raw2json($def);
1008
            $cache->set($cacheKey, $json);
1009
        }
1010
        return $json;
1011
    }
1012
1013
    /**
1014
     * Populates an array of classes in the CMS
1015
     * which allows the user to change the page type.
1016
     *
1017
     * @return SS_List
1018
     */
1019
    public function PageTypes()
1020
    {
1021
        $classes = SiteTree::page_type_classes();
1022
1023
        $result = new ArrayList();
1024
1025
        foreach ($classes as $class) {
1026
            $instance = SiteTree::singleton($class);
1027
            if ($instance instanceof HiddenClass) {
1028
                continue;
1029
            }
1030
1031
            // skip this type if it is restricted
1032
            if ($instance->config()->get('need_permission') && !$this->can(singleton($class)->config()->get('need_permission'))) {
1033
                continue;
1034
            }
1035
1036
            $singularName = $instance->i18n_singular_name();
1037
            $description = $instance->i18n_classDescription();
1038
1039
            $result->push(new ArrayData(array(
1040
                'ClassName' => $class,
1041
                'AddAction' => $singularName,
1042
                'Description' => $description,
1043
                // TODO Sprite support
1044
                'IconURL' => $instance->config()->get('icon'),
1045
                'Title' => $singularName,
1046
            )));
1047
        }
1048
1049
        $result = $result->sort('AddAction');
1050
1051
        return $result;
1052
    }
1053
1054
    /**
1055
     * Get a database record to be managed by the CMS.
1056
     *
1057
     * @param int $id Record ID
1058
     * @param int $versionID optional Version id of the given record
1059
     * @return SiteTree
1060
     */
1061
    public function getRecord($id, $versionID = null)
1062
    {
1063
        if (!$id) {
1064
            return null;
1065
        }
1066
        $treeClass = $this->config()->get('tree_class');
1067
        if ($id instanceof $treeClass) {
1068
            return $id;
1069
        }
1070
        if (substr($id, 0, 3) == 'new') {
1071
            return $this->getNewItem($id);
1072
        }
1073
        if (!is_numeric($id)) {
1074
            return null;
1075
        }
1076
1077
        $currentStage = Versioned::get_reading_mode();
1078
1079
        if ($this->getRequest()->getVar('Version')) {
1080
            $versionID = (int) $this->getRequest()->getVar('Version');
1081
        }
1082
1083
        /** @var SiteTree $record */
1084
        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...
1085
            $record = Versioned::get_version($treeClass, $id, $versionID);
1086
        } else {
1087
            $record = DataObject::get_by_id($treeClass, $id);
1088
        }
1089
1090
        // Then, try getting a record from the live site
1091
        if (!$record) {
1092
            // $record = Versioned::get_one_by_stage($treeClass, "Live", "\"$treeClass\".\"ID\" = $id");
1093
            Versioned::set_stage(Versioned::LIVE);
1094
            singleton($treeClass)->flushCache();
1095
1096
            $record = DataObject::get_by_id($treeClass, $id);
1097
        }
1098
1099
        // Then, try getting a deleted record
1100
        if (!$record) {
1101
            $record = Versioned::get_latest_version($treeClass, $id);
1102
        }
1103
1104
        // Set the reading mode back to what it was.
1105
        Versioned::set_reading_mode($currentStage);
1106
1107
        return $record;
1108
    }
1109
1110
    /**
1111
     * {@inheritdoc}
1112
     *
1113
     * @param HTTPRequest $request
1114
     * @return Form
1115
     */
1116
    public function EditForm($request = null)
1117
    {
1118
        // set page ID from request
1119
        if ($request) {
1120
            // Validate id is present
1121
            $id = $request->param('ID');
1122
            if (!isset($id)) {
1123
                $this->httpError(400);
1124
                return null;
1125
            }
1126
            $this->setCurrentPageID($id);
1127
        }
1128
        return $this->getEditForm();
1129
    }
1130
1131
    /**
1132
     * @param int $id
1133
     * @param FieldList $fields
1134
     * @return Form
1135
     */
1136
    public function getEditForm($id = null, $fields = null)
1137
    {
1138
        // Get record
1139
        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...
1140
            $id = $this->currentPageID();
1141
        }
1142
        /** @var SiteTree $record */
1143
        $record = $this->getRecord($id);
1144
1145
        // Check parent form can be generated
1146
        $form = parent::getEditForm($record, $fields);
0 ignored issues
show
Documentation introduced by
$record is of type object<SilverStripe\CMS\Model\SiteTree>, but the function expects a integer|null.

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1147
        if (!$form || !$record) {
1148
            return $form;
1149
        }
1150
1151
        if (!$fields) {
1152
            $fields = $form->Fields();
1153
        }
1154
1155
        // Add extra fields
1156
        $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...
1157
        $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...
1158
        // Necessary for different subsites
1159
        $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...
1160
        $fields->push($liveLinkField = new HiddenField("LiveLink"));
1161
        $fields->push($stageLinkField = new HiddenField("StageLink"));
1162
        $fields->push($archiveWarningMsgField = new HiddenField("ArchiveWarningMessage"));
1163
        $fields->push(new HiddenField("TreeTitle", false, $record->getTreeTitle()));
0 ignored issues
show
Documentation introduced by
false is of type boolean, but the function expects a null|string.

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1164
1165
        $archiveWarningMsgField->setValue($this->getArchiveWarningMessage($record));
1166
1167
        // Build preview / live links
1168
        $liveLink = $record->getAbsoluteLiveLink();
1169
        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...
1170
            $liveLinkField->setValue($liveLink);
1171
        }
1172
        if (!$deletedFromStage) {
1173
            $stageLink = Controller::join_links($record->AbsoluteLink(), '?stage=Stage');
1174
            if ($stageLink) {
1175
                $stageLinkField->setValue($stageLink);
1176
            }
1177
        }
1178
1179
        // Added in-line to the form, but plucked into different view by LeftAndMain.Preview.js upon load
1180
        /** @skipUpgrade */
1181
        if ($record instanceof CMSPreviewable && !$fields->fieldByName('SilverStripeNavigator')) {
1182
            $navField = new LiteralField('SilverStripeNavigator', $this->getSilverStripeNavigator());
1183
            $navField->setAllowHTML(true);
1184
            $fields->push($navField);
1185
        }
1186
1187
        // getAllCMSActions can be used to completely redefine the action list
1188
        if ($record->hasMethod('getAllCMSActions')) {
1189
            $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...
1190
        } else {
1191
            $actions = $record->getCMSActions();
1192
1193
            // Find and remove action menus that have no actions.
1194
            if ($actions && $actions->count()) {
1195
                /** @var TabSet $tabset */
1196
                $tabset = $actions->fieldByName('ActionMenus');
1197
                if ($tabset) {
1198
                    /** @var Tab $tab */
1199
                    foreach ($tabset->getChildren() as $tab) {
1200
                        if (!$tab->getChildren()->count()) {
1201
                            $tabset->removeByName($tab->getName());
1202
                        }
1203
                    }
1204
                }
1205
            }
1206
        }
1207
1208
        // Use <button> to allow full jQuery UI styling
1209
        $actionsFlattened = $actions->dataFields();
1210
        if ($actionsFlattened) {
1211
            /** @var FormAction $action */
1212
            foreach ($actionsFlattened as $action) {
1213
                $action->setUseButtonTag(true);
1214
            }
1215
        }
1216
1217
        // TODO Can't merge $FormAttributes in template at the moment
1218
        $form->addExtraClass('center ' . $this->BaseCSSClasses());
1219
        // Set validation exemptions for specific actions
1220
        $form->setValidationExemptActions(array(
1221
            'restore',
1222
            'revert',
1223
            'deletefromlive',
1224
            'delete',
1225
            'unpublish',
1226
            'rollback',
1227
            'doRollback',
1228
            'archive',
1229
        ));
1230
1231
        // Announce the capability so the frontend can decide whether to allow preview or not.
1232
        if ($record instanceof CMSPreviewable) {
1233
            $form->addExtraClass('cms-previewable');
1234
        }
1235
        $form->addExtraClass('fill-height flexbox-area-grow');
1236
1237
        if (!$record->canEdit() || $deletedFromStage) {
1238
            $readonlyFields = $form->Fields()->makeReadonly();
1239
            $form->setFields($readonlyFields);
1240
        }
1241
1242
        $form->Fields()->setForm($form);
1243
1244
        $this->extend('updateEditForm', $form);
1245
1246
        // Use custom reqest handler for LeftAndMain requests;
1247
        // CMS Forms cannot be identified solely by name, but also need ID (and sometimes OtherID)
1248
        $form->setRequestHandler(
1249
            LeftAndMainFormRequestHandler::create($form, [$id])
1250
        );
1251
        return $form;
1252
    }
1253
1254
    public function EmptyForm()
1255
    {
1256
        $fields = new FieldList(
1257
            new LabelField('PageDoesntExistLabel', _t('SilverStripe\\CMS\\Controllers\\CMSMain.PAGENOTEXISTS', "This page doesn't exist"))
1258
        );
1259
        $form = parent::EmptyForm();
1260
        $form->setFields($fields);
1261
        $fields->setForm($form);
1262
        return $form;
1263
    }
1264
1265
    /**
1266
     * Build an archive warning message based on the page's children
1267
     *
1268
     * @param SiteTree $record
1269
     * @return string
1270
     */
1271
    protected function getArchiveWarningMessage($record)
1272
    {
1273
        // Get all page's descendants
1274
        $record->collateDescendants(true, $descendants);
0 ignored issues
show
Documentation introduced by
true is of type boolean, but the function expects a string.

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1275
        if (!$descendants) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $descendants of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
1276
            $descendants = [];
1277
        }
1278
1279
        // Get all campaigns that the page and its descendants belong to
1280
        $inChangeSetIDs = ChangeSetItem::get_for_object($record)->column('ChangeSetID');
1281
1282
        foreach ($descendants as $page) {
1283
            $inChangeSetIDs = array_merge($inChangeSetIDs, ChangeSetItem::get_for_object($page)->column('ChangeSetID'));
1284
        }
1285
1286
        if (count($inChangeSetIDs) > 0) {
1287
            $inChangeSets = ChangeSet::get()->filter(['ID' => $inChangeSetIDs, 'State' => ChangeSet::STATE_OPEN]);
1288
        } else {
1289
            $inChangeSets = new ArrayList();
1290
        }
1291
1292
        $numCampaigns = ChangeSet::singleton()->i18n_pluralise($inChangeSets->count());
1293
        $numCampaigns = mb_strtolower($numCampaigns);
1294
1295
        if (count($descendants) > 0 && $inChangeSets->count() > 0) {
1296
            $archiveWarningMsg = _t('SilverStripe\\CMS\\Controllers\\CMSMain.ArchiveWarningWithChildrenAndCampaigns', 'Warning: This page and all of its child pages will be unpublished and automatically removed from their associated {NumCampaigns} before being sent to the archive.\n\nAre you sure you want to proceed?', [ 'NumCampaigns' => $numCampaigns ]);
1297
        } elseif (count($descendants) > 0) {
1298
            $archiveWarningMsg = _t('SilverStripe\\CMS\\Controllers\\CMSMain.ArchiveWarningWithChildren', 'Warning: This page and all of its child pages will be unpublished before being sent to the archive.\n\nAre you sure you want to proceed?');
1299
        } elseif ($inChangeSets->count() > 0) {
1300
            $archiveWarningMsg = _t('SilverStripe\\CMS\\Controllers\\CMSMain.ArchiveWarningWithCampaigns', 'Warning: This page will be unpublished and automatically removed from their associated {NumCampaigns} before being sent to the archive.\n\nAre you sure you want to proceed?', [ 'NumCampaigns' => $numCampaigns ]);
1301
        } else {
1302
            $archiveWarningMsg = _t('SilverStripe\\CMS\\Controllers\\CMSMain.ArchiveWarning', 'Warning: This page will be unpublished before being sent to the archive.\n\nAre you sure you want to proceed?');
1303
        }
1304
1305
        return $archiveWarningMsg;
1306
    }
1307
1308
    /**
1309
     * This method exclusively handles deferred ajax requests to render the
1310
     * pages tree deferred handler (no pjax-fragment)
1311
     *
1312
     * @param HTTPRequest $request
1313
     * @return string HTML
1314
     */
1315
    public function treeview($request)
1316
    {
1317
        // Ensure selected page ID is highlighted
1318
        $pageID = $request->param('ID') ?: 0;
1319
        $this->setCurrentPageID($pageID);
1320
        return $this->renderWith($this->getTemplatesWithSuffix('_TreeView'));
1321
    }
1322
1323
    /**
1324
     * Note: This method exclusively handles top level view of list view
1325
     *
1326
     * @param HTTPRequest $request
1327
     * @return string HTML
1328
     */
1329
    public function listview($request)
1330
    {
1331
        // Ensure selected page ID is highlighted
1332
        $pageID = $request->param('ParentID') ?: 0;
1333
        $this->setCurrentPageID($pageID);
1334
        return $this->getResponseNegotiator()->respond($request);
1335
    }
1336
1337
    /**
1338
     * @param string $default
1339
     * @return string
1340
     */
1341
    public function ViewState($default = 'treeview')
1342
    {
1343
        $mode = $this->getRequest()->requestVar('view')
1344
            ?: $this->getRequest()->param('Action');
1345
        switch ($mode) {
1346
            case 'listview':
1347
            case 'treeview':
1348
                return $mode;
1349
            default:
1350
                return $default;
1351
        }
1352
    }
1353
1354
    /**
1355
     * Callback to request the list of page types allowed under a given page instance.
1356
     * Provides a slower but more precise response over SiteTreeHints
1357
     *
1358
     * @param HTTPRequest $request
1359
     * @return HTTPResponse
1360
     */
1361
    public function childfilter($request)
1362
    {
1363
        // Check valid parent specified
1364
        $parentID = $request->requestVar('ParentID');
1365
        $parent = SiteTree::get()->byID($parentID);
1366
        if (!$parent || !$parent->exists()) {
1367
            return $this->httpError(404);
1368
        }
1369
1370
        // Build hints specific to this class
1371
        // Identify disallows and set globals
1372
        $classes = SiteTree::page_type_classes();
1373
        $disallowedChildren = array();
1374
        foreach ($classes as $class) {
1375
            $obj = singleton($class);
1376
            if ($obj instanceof HiddenClass) {
1377
                continue;
1378
            }
1379
1380
            if (!$obj->canCreate(null, array('Parent' => $parent))) {
1381
                $disallowedChildren[] = $class;
1382
            }
1383
        }
1384
1385
        $this->extend('updateChildFilter', $disallowedChildren, $parentID);
1386
        return $this
1387
            ->getResponse()
1388
            ->addHeader('Content-Type', 'application/json; charset=utf-8')
1389
            ->setBody(Convert::raw2json($disallowedChildren));
1390
    }
1391
1392
    /**
1393
     * Safely reconstruct a selected filter from a given set of query parameters
1394
     *
1395
     * @param array $params Query parameters to use
1396
     * @return CMSSiteTreeFilter The filter class, or null if none present
1397
     * @throws InvalidArgumentException if invalid filter class is passed.
1398
     */
1399
    protected function getQueryFilter($params)
1400
    {
1401
        if (empty($params['FilterClass'])) {
1402
            return null;
1403
        }
1404
        $filterClass = $params['FilterClass'];
1405
        if (!is_subclass_of($filterClass, CMSSiteTreeFilter::class)) {
0 ignored issues
show
Bug introduced by
Due to PHP Bug #53727, is_subclass_of might return inconsistent results on some PHP versions if \SilverStripe\CMS\Contro...MSSiteTreeFilter::class can be an interface. If so, you could instead use ReflectionClass::implementsInterface.
Loading history...
1406
            throw new InvalidArgumentException("Invalid filter class passed: {$filterClass}");
1407
        }
1408
        return $filterClass::create($params);
1409
    }
1410
1411
    /**
1412
     * Returns the pages meet a certain criteria as {@see CMSSiteTreeFilter} or the subpages of a parent page
1413
     * defaulting to no filter and show all pages in first level.
1414
     * Doubles as search results, if any search parameters are set through {@link SearchForm()}.
1415
     *
1416
     * @param array $params Search filter criteria
1417
     * @param int $parentID Optional parent node to filter on (can't be combined with other search criteria)
1418
     * @return SS_List
1419
     * @throws InvalidArgumentException if invalid filter class is passed.
1420
     */
1421
    public function getList($params = array(), $parentID = 0)
1422
    {
1423
        if ($filter = $this->getQueryFilter($params)) {
1424
            return $filter->getFilteredPages();
1425
        } else {
1426
            $list = DataList::create($this->config()->get('tree_class'));
1427
            $parentID = is_numeric($parentID) ? $parentID : 0;
1428
            return $list->filter("ParentID", $parentID);
1429
        }
1430
    }
1431
1432
    /**
1433
     * @return Form
1434
     */
1435
    public function ListViewForm()
1436
    {
1437
        $params = $this->getRequest()->requestVar('q');
1438
        $parentID = $this->getRequest()->param('ParentID');
1439
        $list = $this->getList($params, $parentID);
1440
        $gridFieldConfig = GridFieldConfig::create()->addComponents(
1441
            new GridFieldSortableHeader(),
1442
            new GridFieldDataColumns(),
1443
            new GridFieldPaginator($this->config()->get('page_length'))
1444
        );
1445
        if ($parentID) {
1446
            $linkSpec = $this->LinkListViewChildren('%d');
1447
            $gridFieldConfig->addComponent(
1448
                GridFieldLevelup::create($parentID)
1449
                    ->setLinkSpec($linkSpec)
1450
                    ->setAttributes(array('data-pjax' => 'ListViewForm,Breadcrumbs'))
1451
            );
1452
        }
1453
        $gridField = new GridField('Page', 'Pages', $list, $gridFieldConfig);
1454
        /** @var GridFieldDataColumns $columns */
1455
        $columns = $gridField->getConfig()->getComponentByType('SilverStripe\\Forms\\GridField\\GridFieldDataColumns');
1456
1457
        // Don't allow navigating into children nodes on filtered lists
1458
        $fields = array(
1459
            'getTreeTitle' => _t('SilverStripe\\CMS\\Model\\SiteTree.PAGETITLE', 'Page Title'),
1460
            'singular_name' => _t('SilverStripe\\CMS\\Model\\SiteTree.PAGETYPE', 'Page Type'),
1461
            'LastEdited' => _t('SilverStripe\\CMS\\Model\\SiteTree.LASTUPDATED', 'Last Updated'),
1462
        );
1463
        /** @var GridFieldSortableHeader $sortableHeader */
1464
        $sortableHeader = $gridField->getConfig()->getComponentByType('SilverStripe\\Forms\\GridField\\GridFieldSortableHeader');
1465
        $sortableHeader->setFieldSorting(array('getTreeTitle' => 'Title'));
1466
        $gridField->getState()->ParentID = $parentID;
1467
1468
        if (!$params) {
1469
            $fields = array_merge(array('listChildrenLink' => ''), $fields);
1470
        }
1471
1472
        $columns->setDisplayFields($fields);
1473
        $columns->setFieldCasting(array(
1474
            'Created' => 'DBDatetime->Ago',
1475
            'LastEdited' => 'DBDatetime->FormatFromSettings',
1476
            'getTreeTitle' => 'HTMLFragment'
1477
        ));
1478
1479
        $controller = $this;
1480
        $columns->setFieldFormatting(array(
1481
            'listChildrenLink' => function ($value, &$item) use ($controller) {
1482
                /** @var SiteTree $item */
1483
                $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...
1484
                if ($num) {
1485
                    return sprintf(
1486
                        '<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>',
1487
                        $this->LinkListViewChildren((int)$item->ID),
1488
                        $num
1489
                    );
1490
                }
1491
            },
1492
            'getTreeTitle' => function ($value, &$item) use ($controller) {
1493
                $title = sprintf(
1494
                    '<a class="action-detail" href="%s">%s</a>',
1495
                    Controller::join_links(
1496
                        CMSPageEditController::singleton()->Link('show'),
1497
                        (int)$item->ID
1498
                    ),
1499
                    $item->TreeTitle // returns HTML, does its own escaping
1500
                );
1501
                $breadcrumbs = $item->Breadcrumbs(20, true, false, true, '/');
1502
                // Remove item's tile
1503
                $breadcrumbs = preg_replace('/[^\/]+$/', '', trim($breadcrumbs));
1504
                // Trim spaces around delimiters
1505
                $breadcrumbs = preg_replace('/\s?\/\s?/', '/', trim($breadcrumbs));
1506
                return $title . sprintf('<p class="small cms-list__item-breadcrumbs">%s</p>', $breadcrumbs);
1507
            }
1508
        ));
1509
1510
        $negotiator = $this->getResponseNegotiator();
1511
        $listview = Form::create(
1512
            $this,
1513
            'ListViewForm',
1514
            new FieldList($gridField),
1515
            new FieldList()
1516
        )->setHTMLID('Form_ListViewForm');
1517
        $listview->setAttribute('data-pjax-fragment', 'ListViewForm');
1518 View Code Duplication
        $listview->setValidationResponseCallback(function (ValidationResult $errors) use ($negotiator, $listview) {
0 ignored issues
show
Unused Code introduced by
The parameter $errors is not used and could be removed.

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

Loading history...
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1519
            $request = $this->getRequest();
1520
            if ($request->isAjax() && $negotiator) {
1521
                $result = $listview->forTemplate();
1522
                return $negotiator->respond($request, array(
1523
                    'CurrentForm' => function () use ($result) {
1524
                        return $result;
1525
                    }
1526
                ));
1527
            }
1528
        });
1529
1530
        $this->extend('updateListView', $listview);
1531
1532
        $listview->disableSecurityToken();
1533
        return $listview;
1534
    }
1535
1536
    public function currentPageID()
1537
    {
1538
        $id = parent::currentPageID();
1539
1540
        $this->extend('updateCurrentPageID', $id);
1541
1542
        return $id;
1543
    }
1544
1545
    //------------------------------------------------------------------------------------------//
1546
    // Data saving handlers
1547
1548
    /**
1549
     * Save and Publish page handler
1550
     *
1551
     * @param array $data
1552
     * @param Form $form
1553
     * @return HTTPResponse
1554
     * @throws HTTPResponse_Exception
1555
     */
1556
    public function save($data, $form)
1557
    {
1558
        $className = $this->config()->get('tree_class');
1559
1560
        // Existing or new record?
1561
        $id = $data['ID'];
1562
        if (substr($id, 0, 3) != 'new') {
1563
            /** @var SiteTree $record */
1564
            $record = DataObject::get_by_id($className, $id);
1565
            // Check edit permissions
1566
            if ($record && !$record->canEdit()) {
1567
                return Security::permissionFailure($this);
1568
            }
1569
            if (!$record || !$record->ID) {
1570
                throw new HTTPResponse_Exception("Bad record ID #$id", 404);
1571
            }
1572
        } else {
1573
            if (!$className::singleton()->canCreate()) {
1574
                return Security::permissionFailure($this);
1575
            }
1576
            $record = $this->getNewItem($id, false);
1577
        }
1578
1579
        // Check publishing permissions
1580
        $doPublish = !empty($data['publish']);
1581
        if ($record && $doPublish && !$record->canPublish()) {
1582
            return Security::permissionFailure($this);
1583
        }
1584
1585
        // TODO Coupling to SiteTree
1586
        $record->HasBrokenLink = 0;
1587
        $record->HasBrokenFile = 0;
1588
1589
        if (!$record->ObsoleteClassName) {
1590
            $record->writeWithoutVersion();
1591
        }
1592
1593
        // Update the class instance if necessary
1594
        if (isset($data['ClassName']) && $data['ClassName'] != $record->ClassName) {
1595
            // Replace $record with a new instance of the new class
1596
            $newClassName = $data['ClassName'];
1597
            $record = $record->newClassInstance($newClassName);
1598
        }
1599
1600
        // save form data into record
1601
        $form->saveInto($record);
1602
        $record->write();
1603
1604
        // If the 'Save & Publish' button was clicked, also publish the page
1605
        if ($doPublish) {
1606
            $record->publishRecursive();
1607
            $message = _t(
1608
                'SilverStripe\\CMS\\Controllers\\CMSMain.PUBLISHED',
1609
                "Published '{title}' successfully.",
1610
                ['title' => $record->Title]
1611
            );
1612
        } else {
1613
            $message = _t(
1614
                'SilverStripe\\CMS\\Controllers\\CMSMain.SAVED',
1615
                "Saved '{title}' successfully.",
1616
                ['title' => $record->Title]
1617
            );
1618
        }
1619
1620
        $this->getResponse()->addHeader('X-Status', rawurlencode($message));
1621
        return $this->getResponseNegotiator()->respond($this->getRequest());
1622
    }
1623
1624
    /**
1625
     * @uses LeftAndMainExtension->augmentNewSiteTreeItem()
1626
     *
1627
     * @param int|string $id
1628
     * @param bool $setID
1629
     * @return mixed|DataObject
1630
     * @throws HTTPResponse_Exception
1631
     */
1632
    public function getNewItem($id, $setID = true)
1633
    {
1634
        $parentClass = $this->config()->get('tree_class');
1635
        list(, $className, $parentID) = array_pad(explode('-', $id), 3, null);
1636
1637
        if (!is_a($className, $parentClass, true)) {
1638
            $response = Security::permissionFailure($this);
1639
            if (!$response) {
1640
                $response = $this->getResponse();
1641
            }
1642
            throw new HTTPResponse_Exception($response);
1643
        }
1644
1645
        /** @var SiteTree $newItem */
1646
        $newItem = Injector::inst()->create($className);
1647
        $newItem->Title = _t(
1648
            'SilverStripe\\CMS\\Controllers\\CMSMain.NEWPAGE',
1649
            "New {pagetype}",
1650
            'followed by a page type title',
1651
            array('pagetype' => singleton($className)->i18n_singular_name())
1652
        );
1653
        $newItem->ClassName = $className;
1654
        $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...
1655
1656
        // DataObject::fieldExists only checks the current class, not the hierarchy
1657
        // This allows the CMS to set the correct sort value
1658
        if ($newItem->castingHelper('Sort')) {
1659
            $maxSort = DB::prepared_query(
1660
                'SELECT MAX("Sort") FROM "SiteTree" WHERE "ParentID" = ?',
1661
                array($parentID)
1662
            )->value();
1663
            $newItem->Sort = (int)$maxSort + 1;
1664
        }
1665
1666
        if ($setID && $id) {
1667
            $newItem->ID = $id;
1668
        }
1669
1670
        # Some modules like subsites add extra fields that need to be set when the new item is created
1671
        $this->extend('augmentNewSiteTreeItem', $newItem);
1672
1673
        return $newItem;
1674
    }
1675
1676
    /**
1677
     * Actually perform the publication step
1678
     *
1679
     * @param Versioned|DataObject $record
1680
     * @return mixed
1681
     */
1682
    public function performPublish($record)
1683
    {
1684
        if ($record && !$record->canPublish()) {
0 ignored issues
show
Bug introduced by
The method canPublish does only exist in SilverStripe\Versioned\Versioned, but not in SilverStripe\ORM\DataObject.

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

Let’s take a look at an example:

class A
{
    public function foo() { }
}

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

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

Available Fixes

  1. Add an additional type-check:

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

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

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

Let’s take a look at an example:

class A
{
    public function foo() { }
}

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

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

Available Fixes

  1. Add an additional type-check:

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

    function someFunction(B $x) { /** ... */ }
    
Loading history...
1689
    }
1690
1691
    /**
1692
     * Reverts a page by publishing it to live.
1693
     * Use {@link restorepage()} if you want to restore a page
1694
     * which was deleted from draft without publishing.
1695
     *
1696
     * @uses SiteTree->doRevertToLive()
1697
     *
1698
     * @param array $data
1699
     * @param Form $form
1700
     * @return HTTPResponse
1701
     * @throws HTTPResponse_Exception
1702
     */
1703
    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...
1704
    {
1705
        if (!isset($data['ID'])) {
1706
            throw new HTTPResponse_Exception("Please pass an ID in the form content", 400);
1707
        }
1708
1709
        $id = (int) $data['ID'];
1710
        $restoredPage = Versioned::get_latest_version(SiteTree::class, $id);
1711
        if (!$restoredPage) {
1712
            throw new HTTPResponse_Exception("SiteTree #$id not found", 400);
1713
        }
1714
1715
        /** @var SiteTree $record */
1716
        $record = Versioned::get_one_by_stage('SilverStripe\\CMS\\Model\\SiteTree', 'Live', array(
1717
            '"SiteTree_Live"."ID"' => $id
1718
        ));
1719
1720
        // a user can restore a page without publication rights, as it just adds a new draft state
1721
        // (this action should just be available when page has been "deleted from draft")
1722
        if ($record && !$record->canEdit()) {
1723
            return Security::permissionFailure($this);
1724
        }
1725
        if (!$record || !$record->ID) {
1726
            throw new HTTPResponse_Exception("Bad record ID #$id", 404);
1727
        }
1728
1729
        $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...
1730
1731
        $this->getResponse()->addHeader(
1732
            'X-Status',
1733
            rawurlencode(_t(
1734
                'SilverStripe\\CMS\\Controllers\\CMSMain.RESTORED',
1735
                "Restored '{title}' successfully",
1736
                'Param {title} is a title',
1737
                array('title' => $record->Title)
1738
            ))
1739
        );
1740
1741
        return $this->getResponseNegotiator()->respond($this->getRequest());
1742
    }
1743
1744
    /**
1745
     * Delete the current page from draft stage.
1746
     *
1747
     * @see deletefromlive()
1748
     *
1749
     * @param array $data
1750
     * @param Form $form
1751
     * @return HTTPResponse
1752
     * @throws HTTPResponse_Exception
1753
     */
1754 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...
1755
    {
1756
        $id = $data['ID'];
1757
        $record = SiteTree::get()->byID($id);
1758
        if ($record && !$record->canDelete()) {
1759
            return Security::permissionFailure();
1760
        }
1761
        if (!$record || !$record->ID) {
1762
            throw new HTTPResponse_Exception("Bad record ID #$id", 404);
1763
        }
1764
1765
        // Delete record
1766
        $record->delete();
1767
1768
        $this->getResponse()->addHeader(
1769
            'X-Status',
1770
            rawurlencode(_t(
1771
                __CLASS__ . '.REMOVEDPAGEFROMDRAFT',
1772
                "Removed '{title}' from the draft site",
1773
                ['title' => $record->Title]
1774
            ))
1775
        );
1776
1777
        // Even if the record has been deleted from stage and live, it can be viewed in "archive mode"
1778
        return $this->getResponseNegotiator()->respond($this->getRequest());
1779
    }
1780
1781
    /**
1782
     * Delete this page from both live and stage
1783
     *
1784
     * @param array $data
1785
     * @param Form $form
1786
     * @return HTTPResponse
1787
     * @throws HTTPResponse_Exception
1788
     */
1789 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...
1790
    {
1791
        $id = $data['ID'];
1792
        /** @var SiteTree $record */
1793
        $record = SiteTree::get()->byID($id);
1794
        if (!$record || !$record->exists()) {
1795
            throw new HTTPResponse_Exception("Bad record ID #$id", 404);
1796
        }
1797
        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...
1798
            return Security::permissionFailure();
1799
        }
1800
1801
        // Archive record
1802
        $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...
1803
1804
        $this->getResponse()->addHeader(
1805
            'X-Status',
1806
            rawurlencode(_t(
1807
                __CLASS__ . '.ARCHIVEDPAGE',
1808
                "Archived page '{title}'",
1809
                ['title' => $record->Title]
1810
            ))
1811
        );
1812
1813
        // Even if the record has been deleted from stage and live, it can be viewed in "archive mode"
1814
        return $this->getResponseNegotiator()->respond($this->getRequest());
1815
    }
1816
1817
    public function publish($data, $form)
1818
    {
1819
        $data['publish'] = '1';
1820
1821
        return $this->save($data, $form);
1822
    }
1823
1824 View Code Duplication
    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...
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...
1825
    {
1826
        $className = $this->config()->get('tree_class');
1827
        /** @var SiteTree $record */
1828
        $record = DataObject::get_by_id($className, $data['ID']);
1829
1830
        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...
1831
            return Security::permissionFailure($this);
1832
        }
1833
        if (!$record || !$record->ID) {
1834
            throw new HTTPResponse_Exception("Bad record ID #" . (int)$data['ID'], 404);
1835
        }
1836
1837
        $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...
1838
1839
        $this->getResponse()->addHeader(
1840
            'X-Status',
1841
            rawurlencode(_t('SilverStripe\\CMS\\Controllers\\CMSMain.REMOVEDPAGE', "Removed '{title}' from the published site", array('title' => $record->Title)))
1842
        );
1843
1844
        return $this->getResponseNegotiator()->respond($this->getRequest());
1845
    }
1846
1847
    /**
1848
     * @return HTTPResponse
1849
     */
1850
    public function rollback()
1851
    {
1852
        return $this->doRollback(array(
1853
            'ID' => $this->currentPageID(),
1854
            'Version' => $this->getRequest()->param('VersionID')
1855
        ), 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...
1856
    }
1857
1858
    /**
1859
     * Rolls a site back to a given version ID
1860
     *
1861
     * @param array $data
1862
     * @param Form $form
1863
     * @return HTTPResponse
1864
     */
1865
    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...
1866
    {
1867
        $this->extend('onBeforeRollback', $data['ID']);
1868
1869
        $id = (isset($data['ID'])) ? (int) $data['ID'] : null;
1870
        $version = (isset($data['Version'])) ? (int) $data['Version'] : null;
1871
1872
        /** @var DataObject|Versioned $record */
1873
        $record = DataObject::get_by_id($this->config()->get('tree_class'), $id);
1874
        if ($record && !$record->canEdit()) {
0 ignored issues
show
Bug introduced by
The method canEdit does only exist in SilverStripe\ORM\DataObject, but not in SilverStripe\Versioned\Versioned.

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

Let’s take a look at an example:

class A
{
    public function foo() { }
}

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

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

Available Fixes

  1. Add an additional type-check:

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

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

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

Let’s take a look at an example:

class A
{
    public function foo() { }
}

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

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

Available Fixes

  1. Add an additional type-check:

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

    function someFunction(B $x) { /** ... */ }
    
Loading history...
1880
            $message = _t(
1881
                __CLASS__ . '.ROLLEDBACKVERSIONv2',
1882
                "Rolled back to version #{version}.",
1883
                array('version' => $data['Version'])
1884
            );
1885
        } else {
1886
            $record->doRevertToLive();
0 ignored issues
show
Bug introduced by
The method doRevertToLive does only exist in SilverStripe\Versioned\Versioned, but not in SilverStripe\ORM\DataObject.

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

Let’s take a look at an example:

class A
{
    public function foo() { }
}

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

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

Available Fixes

  1. Add an additional type-check:

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

    function someFunction(B $x) { /** ... */ }
    
Loading history...
1887
            $message = _t(
1888
                __CLASS__ . '.ROLLEDBACKPUBv2',
1889
                "Rolled back to published version."
1890
            );
1891
        }
1892
1893
        $this->getResponse()->addHeader('X-Status', rawurlencode($message));
1894
1895
        // Can be used in different contexts: In normal page edit view, in which case the redirect won't have any effect.
1896
        // Or in history view, in which case a revert causes the CMS to re-load the edit view.
1897
        // The X-Pjax header forces a "full" content refresh on redirect.
1898
        $url = Controller::join_links(CMSPageEditController::singleton()->Link('show'), $record->ID);
1899
        $this->getResponse()->addHeader('X-ControllerURL', $url);
1900
        $this->getRequest()->addHeader('X-Pjax', 'Content');
1901
        $this->getResponse()->addHeader('X-Pjax', 'Content');
1902
1903
        return $this->getResponseNegotiator()->respond($this->getRequest());
1904
    }
1905
1906
    /**
1907
     * Batch Actions Handler
1908
     */
1909
    public function batchactions()
1910
    {
1911
        return new CMSBatchActionHandler($this, 'batchactions');
1912
    }
1913
1914
    public function BatchActionParameters()
1915
    {
1916
        $batchActions = CMSBatchActionHandler::config()->batch_actions;
1917
1918
        $forms = array();
1919
        foreach ($batchActions as $urlSegment => $batchAction) {
1920
            $SNG_action = singleton($batchAction);
1921
            if ($SNG_action->canView() && $fieldset = $SNG_action->getParameterFields()) {
1922
                $formHtml = '';
1923
                /** @var FormField $field */
1924
                foreach ($fieldset as $field) {
1925
                    $formHtml .= $field->Field();
1926
                }
1927
                $forms[$urlSegment] = $formHtml;
1928
            }
1929
        }
1930
        $pageHtml = '';
1931
        foreach ($forms as $urlSegment => $html) {
1932
            $pageHtml .= "<div class=\"params\" id=\"BatchActionParameters_$urlSegment\">$html</div>\n\n";
1933
        }
1934
        return new LiteralField("BatchActionParameters", '<div id="BatchActionParameters" style="display:none">'.$pageHtml.'</div>');
1935
    }
1936
    /**
1937
     * Returns a list of batch actions
1938
     */
1939
    public function BatchActionList()
1940
    {
1941
        return $this->batchactions()->batchActionList();
1942
    }
1943
1944
    public function publishall($request)
1945
    {
1946
        if (!Permission::check('ADMIN')) {
1947
            return Security::permissionFailure($this);
1948
        }
1949
1950
        Environment::increaseTimeLimitTo();
1951
        Environment::increaseMemoryLimitTo();
1952
1953
        $response = "";
1954
1955
        if (isset($this->requestParams['confirm'])) {
1956
            // Protect against CSRF on destructive action
1957
            if (!SecurityToken::inst()->checkRequest($request)) {
1958
                return $this->httpError(400);
1959
            }
1960
1961
            $start = 0;
1962
            $pages = SiteTree::get()->limit("$start,30");
1963
            $count = 0;
1964
            while ($pages) {
1965
                /** @var SiteTree $page */
1966
                foreach ($pages as $page) {
1967
                    if ($page && !$page->canPublish()) {
1968
                        return Security::permissionFailure($this);
1969
                    }
1970
1971
                    $page->publishRecursive();
1972
                    $page->destroy();
1973
                    unset($page);
1974
                    $count++;
1975
                    $response .= "<li>$count</li>";
1976
                }
1977
                if ($pages->count() > 29) {
1978
                    $start += 30;
1979
                    $pages = SiteTree::get()->limit("$start,30");
1980
                } else {
1981
                    break;
1982
                }
1983
            }
1984
            $response .= _t('SilverStripe\\CMS\\Controllers\\CMSMain.PUBPAGES', "Done: Published {count} pages", array('count' => $count));
1985
        } else {
1986
            $token = SecurityToken::inst();
1987
            $fields = new FieldList();
1988
            $token->updateFieldSet($fields);
1989
            $tokenField = $fields->first();
1990
            $tokenHtml = ($tokenField) ? $tokenField->FieldHolder() : '';
1991
            $publishAllDescription = _t(
1992
                'SilverStripe\\CMS\\Controllers\\CMSMain.PUBALLFUN2',
1993
                'Pressing this button will do the equivalent of going to every page and pressing "publish".  '
1994
                . 'It\'s intended to be used after there have been massive edits of the content, such as when '
1995
                . 'the site was first built.'
1996
            );
1997
            $response .= '<h1>' . _t('SilverStripe\\CMS\\Controllers\\CMSMain.PUBALLFUN', '"Publish All" functionality') . '</h1>
1998
				<p>' . $publishAllDescription . '</p>
1999
				<form method="post" action="publishall">
2000
					<input type="submit" name="confirm" value="'
2001
                    . _t('SilverStripe\\CMS\\Controllers\\CMSMain.PUBALLCONFIRM', "Please publish every page in the site, copying content stage to live", 'Confirmation button') .'" />'
2002
                    . $tokenHtml .
2003
                '</form>';
2004
        }
2005
2006
        return $response;
2007
    }
2008
2009
    /**
2010
     * Restore a completely deleted page from the SiteTree_versions table.
2011
     *
2012
     * @param array $data
2013
     * @param Form $form
2014
     * @return HTTPResponse
2015
     */
2016
    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...
2017
    {
2018
        if (!isset($data['ID']) || !is_numeric($data['ID'])) {
2019
            return new HTTPResponse("Please pass an ID in the form content", 400);
2020
        }
2021
2022
        $id = (int)$data['ID'];
2023
        /** @var SiteTree $restoredPage */
2024
        $restoredPage = Versioned::get_latest_version(SiteTree::class, $id);
2025
        if (!$restoredPage) {
2026
            return new HTTPResponse("SiteTree #$id not found", 400);
2027
        }
2028
2029
        $restoredPage = $restoredPage->doRestoreToStage();
2030
2031
        $this->getResponse()->addHeader(
2032
            'X-Status',
2033
            rawurlencode(_t(
2034
                'SilverStripe\\CMS\\Controllers\\CMSMain.RESTORED',
2035
                "Restored '{title}' successfully",
2036
                array('title' => $restoredPage->Title)
2037
            ))
2038
        );
2039
2040
        return $this->getResponseNegotiator()->respond($this->getRequest());
2041
    }
2042
2043
    public function duplicate($request)
2044
    {
2045
        // Protect against CSRF on destructive action
2046
        if (!SecurityToken::inst()->checkRequest($request)) {
2047
            return $this->httpError(400);
2048
        }
2049
2050
        if (($id = $this->urlParams['ID']) && is_numeric($id)) {
2051
            /** @var SiteTree $page */
2052
            $page = SiteTree::get()->byID($id);
2053 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...
2054
                return Security::permissionFailure($this);
2055
            }
2056
            if (!$page || !$page->ID) {
2057
                throw new HTTPResponse_Exception("Bad record ID #$id", 404);
2058
            }
2059
2060
            $newPage = $page->duplicate();
2061
2062
            // ParentID can be hard-set in the URL.  This is useful for pages with multiple parents
2063
            if (isset($_GET['parentID']) && is_numeric($_GET['parentID'])) {
2064
                $newPage->ParentID = $_GET['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...
2065
                $newPage->write();
2066
            }
2067
2068
            $this->getResponse()->addHeader(
2069
                'X-Status',
2070
                rawurlencode(_t(
2071
                    'SilverStripe\\CMS\\Controllers\\CMSMain.DUPLICATED',
2072
                    "Duplicated '{title}' successfully",
2073
                    array('title' => $newPage->Title)
2074
                ))
2075
            );
2076
            $url = Controller::join_links(CMSPageEditController::singleton()->Link('show'), $newPage->ID);
2077
            $this->getResponse()->addHeader('X-ControllerURL', $url);
2078
            $this->getRequest()->addHeader('X-Pjax', 'Content');
2079
            $this->getResponse()->addHeader('X-Pjax', 'Content');
2080
2081
            return $this->getResponseNegotiator()->respond($this->getRequest());
2082
        } else {
2083
            return new HTTPResponse("CMSMain::duplicate() Bad ID: '$id'", 400);
2084
        }
2085
    }
2086
2087
    public function duplicatewithchildren($request)
2088
    {
2089
        // Protect against CSRF on destructive action
2090
        if (!SecurityToken::inst()->checkRequest($request)) {
2091
            return $this->httpError(400);
2092
        }
2093
        Environment::increaseTimeLimitTo();
2094
        if (($id = $this->urlParams['ID']) && is_numeric($id)) {
2095
            /** @var SiteTree $page */
2096
            $page = SiteTree::get()->byID($id);
2097 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...
2098
                return Security::permissionFailure($this);
2099
            }
2100
            if (!$page || !$page->ID) {
2101
                throw new HTTPResponse_Exception("Bad record ID #$id", 404);
2102
            }
2103
2104
            $newPage = $page->duplicateWithChildren();
2105
2106
            $this->getResponse()->addHeader(
2107
                'X-Status',
2108
                rawurlencode(_t(
2109
                    'SilverStripe\\CMS\\Controllers\\CMSMain.DUPLICATEDWITHCHILDREN',
2110
                    "Duplicated '{title}' and children successfully",
2111
                    array('title' => $newPage->Title)
2112
                ))
2113
            );
2114
            $url = Controller::join_links(CMSPageEditController::singleton()->Link('show'), $newPage->ID);
2115
            $this->getResponse()->addHeader('X-ControllerURL', $url);
2116
            $this->getRequest()->addHeader('X-Pjax', 'Content');
2117
            $this->getResponse()->addHeader('X-Pjax', 'Content');
2118
2119
            return $this->getResponseNegotiator()->respond($this->getRequest());
2120
        } else {
2121
            return new HTTPResponse("CMSMain::duplicatewithchildren() Bad ID: '$id'", 400);
2122
        }
2123
    }
2124
2125
    public function providePermissions()
2126
    {
2127
        $title = CMSPagesController::menu_title();
2128
        return array(
2129
            "CMS_ACCESS_CMSMain" => array(
2130
                'name' => _t('SilverStripe\\CMS\\Controllers\\CMSMain.ACCESS', "Access to '{title}' section", array('title' => $title)),
2131
                'category' => _t('SilverStripe\\Security\\Permission.CMS_ACCESS_CATEGORY', 'CMS Access'),
2132
                'help' => _t(
2133
                    'SilverStripe\\CMS\\Controllers\\CMSMain.ACCESS_HELP',
2134
                    '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".'
2135
                ),
2136
                'sort' => -99 // below "CMS_ACCESS_LeftAndMain", but above everything else
2137
            )
2138
        );
2139
    }
2140
2141
    /**
2142
     * Get title for root CMS node
2143
     *
2144
     * @return string
2145
     */
2146
    protected function getCMSTreeTitle()
2147
    {
2148
        $rootTitle = SiteConfig::current_site_config()->Title;
2149
        $this->extend('updateCMSTreeTitle', $rootTitle);
2150
        return $rootTitle;
2151
    }
2152
}
2153