Completed
Pull Request — master (#1812)
by Ingo
02:45
created

CMSMain::SearchForm()   B

Complexity

Conditions 2
Paths 2

Size

Total Lines 76
Code Lines 45

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 76
rs 8.9667
cc 2
eloc 45
nc 2
nop 0

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
namespace SilverStripe\CMS\Controllers;
4
5
use SilverStripe\Admin\AdminRootController;
6
use SilverStripe\Admin\CMSBatchActionHandler;
7
use SilverStripe\Admin\LeftAndMain_SearchFilter;
8
use SilverStripe\Admin\LeftAndMainFormRequestHandler;
9
use SilverStripe\CMS\Model\VirtualPage;
10
use SilverStripe\Forms\Tab;
11
use SilverStripe\ORM\CMSPreviewable;
12
use SilverStripe\Admin\LeftAndMain;
13
use SilverStripe\CMS\BatchActions\CMSBatchAction_Archive;
14
use SilverStripe\CMS\BatchActions\CMSBatchAction_Publish;
15
use SilverStripe\CMS\BatchActions\CMSBatchAction_Restore;
16
use SilverStripe\CMS\BatchActions\CMSBatchAction_Unpublish;
17
use SilverStripe\CMS\Model\CurrentPageIdentifier;
18
use SilverStripe\CMS\Model\RedirectorPage;
19
use SilverStripe\CMS\Model\SiteTree;
20
use SilverStripe\Control\Controller;
21
use SilverStripe\Control\Director;
22
use SilverStripe\Control\Session;
23
use SilverStripe\Control\HTTPRequest;
24
use SilverStripe\Control\HTTPResponse;
25
use SilverStripe\Control\HTTPResponse_Exception;
26
use SilverStripe\Core\Convert;
27
use SilverStripe\Core\Injector\Injector;
28
use Psr\SimpleCache\CacheInterface;
29
use SilverStripe\Forms\DateField;
30
use SilverStripe\Forms\DropdownField;
31
use SilverStripe\Forms\FieldGroup;
32
use SilverStripe\Forms\FieldList;
33
use SilverStripe\Forms\Form;
34
use SilverStripe\Forms\FormAction;
35
use SilverStripe\Forms\FormField;
36
use SilverStripe\Forms\GridField\GridField;
37
use SilverStripe\Forms\GridField\GridFieldConfig;
38
use SilverStripe\Forms\GridField\GridFieldDataColumns;
39
use SilverStripe\Forms\GridField\GridFieldLevelup;
40
use SilverStripe\Forms\GridField\GridFieldPaginator;
41
use SilverStripe\Forms\GridField\GridFieldSortableHeader;
42
use SilverStripe\Forms\HiddenField;
43
use SilverStripe\Forms\LabelField;
44
use SilverStripe\Forms\LiteralField;
45
use SilverStripe\Forms\TabSet;
46
use SilverStripe\Forms\TextField;
47
use SilverStripe\ORM\ArrayList;
48
use SilverStripe\ORM\DataList;
49
use SilverStripe\ORM\DataObject;
50
use SilverStripe\ORM\DB;
51
use SilverStripe\ORM\FieldType\DBHTMLText;
52
use SilverStripe\ORM\HiddenClass;
53
use SilverStripe\ORM\Hierarchy\MarkedSet;
54
use SilverStripe\ORM\SS_List;
55
use SilverStripe\ORM\ValidationResult;
56
use SilverStripe\SiteConfig\SiteConfig;
57
use SilverStripe\Versioned\Versioned;
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\View\ArrayData;
64
use SilverStripe\View\Requirements;
65
use Translatable;
66
use InvalidArgumentException;
67
use SilverStripe\Versioned\ChangeSet;
68
use SilverStripe\Versioned\ChangeSetItem;
69
70
/**
71
 * The main "content" area of the CMS.
72
 *
73
 * This class creates a 2-frame layout - left-tree and right-form - to sit beneath the main
74
 * admin menu.
75
 *
76
 * @todo Create some base classes to contain the generic functionality that will be replicated.
77
 *
78
 * @mixin LeftAndMainPageIconsExtension
79
 */
80
class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionProvider
81
{
82
83
    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...
84
85
    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...
86
87
    // Maintain a lower priority than other administration sections
88
    // so that Director does not think they are actions of CMSMain
89
    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...
90
91
    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...
92
93
    private static $menu_icon_class = 'font-icon-sitemap';
94
95
    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...
96
97
    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...
98
99
    private static $subitem_class = Member::class;
100
101
    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...
102
103
    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...
104
105
    /**
106
     * Amount of results showing on a single page.
107
     *
108
     * @config
109
     * @var int
110
     */
111
    private static $page_length = 15;
112
113
    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...
114
        'archive',
115
        'deleteitems',
116
        'DeleteItemsForm',
117
        'dialog',
118
        'duplicate',
119
        'duplicatewithchildren',
120
        'publishall',
121
        'publishitems',
122
        'PublishItemsForm',
123
        'submit',
124
        'EditForm',
125
        'SearchForm',
126
        'SiteTreeAsUL',
127
        'getshowdeletedsubtree',
128
        'savetreenode',
129
        'getsubtree',
130
        'updatetreenodes',
131
        'batchactions',
132
        'treeview',
133
        'listview',
134
        'ListViewForm',
135
        'childfilter',
136
    );
137
138
    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...
139
        'EditForm/$ID' => 'EditForm',
140
    ];
141
142
    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...
143
        'TreeIsFiltered' => 'Boolean',
144
        'AddForm' => 'HTMLFragment',
145
        'LinkPages' => 'Text',
146
        'Link' => 'Text',
147
        'ListViewForm' => 'HTMLFragment',
148
        'ExtraTreeTools' => 'HTMLFragment',
149
        'PageList' => 'HTMLFragment',
150
        'PageListSidebar' => 'HTMLFragment',
151
        'SiteTreeHints' => 'HTMLFragment',
152
        'SecurityID' => 'Text',
153
        'SiteTreeAsUL' => 'HTMLFragment',
154
    );
155
156
    protected function init()
157
    {
158
        // set reading lang
159
        if (SiteTree::has_extension('Translatable') && !$this->getRequest()->isAjax()) {
160
            Translatable::choose_site_locale(array_keys(Translatable::get_existing_content_languages('SilverStripe\\CMS\\Model\\SiteTree')));
161
        }
162
163
        parent::init();
164
165
        Requirements::javascript(CMS_DIR . '/client/dist/js/bundle.js');
166
        Requirements::javascript(CMS_DIR . '/client/dist/js/SilverStripeNavigator.js');
167
        Requirements::css(CMS_DIR . '/client/dist/styles/bundle.css');
168
        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...
169
        Requirements::add_i18n_javascript(CMS_DIR . '/client/lang', false, true);
170
171
        CMSBatchActionHandler::register('restore', CMSBatchAction_Restore::class);
172
        CMSBatchActionHandler::register('archive', CMSBatchAction_Archive::class);
173
        CMSBatchActionHandler::register('unpublish', CMSBatchAction_Unpublish::class);
174
        CMSBatchActionHandler::register('publish', CMSBatchAction_Publish::class);
175
    }
176
177
    public function index($request)
178
    {
179
        // In case we're not showing a specific record, explicitly remove any session state,
180
        // to avoid it being highlighted in the tree, and causing an edit form to show.
181
        if (!$request->param('Action')) {
182
            $this->setCurrentPageID(null);
183
        }
184
185
        return parent::index($request);
186
    }
187
188
    public function getResponseNegotiator()
189
    {
190
        $negotiator = parent::getResponseNegotiator();
191
192
        // ListViewForm
193
        $negotiator->setCallback('ListViewForm', function () {
194
            return $this->ListViewForm()->forTemplate();
195
        });
196
197
        // PageList view
198
        $negotiator->setCallback('Content-PageList', function () {
199
            return $this->PageList()->forTemplate();
200
        });
201
202
        // PageList view for edit controller
203
        $negotiator->setCallback('Content-PageList-Sidebar', function () {
204
            return $this->PageListSidebar()->forTemplate();
205
        });
206
207
        return $negotiator;
208
    }
209
210
    /**
211
     * Get pages listing area
212
     *
213
     * @return DBHTMLText
214
     */
215
    public function PageList()
216
    {
217
        return $this->renderWith($this->getTemplatesWithSuffix('_PageList'));
218
    }
219
220
    /**
221
     * Page list view for edit-form
222
     *
223
     * @return DBHTMLText
224
     */
225
    public function PageListSidebar()
226
    {
227
        return $this->renderWith($this->getTemplatesWithSuffix('_PageList_Sidebar'));
228
    }
229
230
    /**
231
     * If this is set to true, the "switchView" context in the
232
     * template is shown, with links to the staging and publish site.
233
     *
234
     * @return boolean
235
     */
236
    public function ShowSwitchView()
237
    {
238
        return true;
239
    }
240
241
    /**
242
     * Overloads the LeftAndMain::ShowView. Allows to pass a page as a parameter, so we are able
243
     * to switch view also for archived versions.
244
     *
245
     * @param SiteTree $page
246
     * @return array
247
     */
248
    public function SwitchView($page = null)
249
    {
250
        if (!$page) {
251
            $page = $this->currentPage();
252
        }
253
254
        if ($page) {
255
            $nav = SilverStripeNavigator::get_for_record($page);
256
            return $nav['items'];
257
        }
258
    }
259
260
    //------------------------------------------------------------------------------------------//
261
    // Main controllers
262
263
    //------------------------------------------------------------------------------------------//
264
    // Main UI components
265
266
    /**
267
     * Override {@link LeftAndMain} Link to allow blank URL segment for CMSMain.
268
     *
269
     * @param string|null $action Action to link to.
270
     * @return string
271
     */
272
    public function Link($action = null)
273
    {
274
        $link = Controller::join_links(
275
            AdminRootController::admin_url(),
276
            $this->stat('url_segment'), // in case we want to change the segment
277
            '/', // trailing slash needed if $action is null!
278
            "$action"
279
        );
280
        $this->extend('updateLink', $link);
281
        return $link;
282
    }
283
284
    public function LinkPages()
285
    {
286
        return CMSPagesController::singleton()->Link();
287
    }
288
289
    public function LinkPagesWithSearch()
290
    {
291
        return $this->LinkWithSearch($this->LinkPages());
292
    }
293
294
    /**
295
     * Get link to tree view
296
     *
297
     * @return string
298
     */
299
    public function LinkTreeView()
300
    {
301
        // Tree view is just default link to main pages section (no /treeview suffix)
302
        return $this->LinkWithSearch(CMSMain::singleton()->Link());
303
    }
304
305
    /**
306
     * Get link to list view
307
     *
308
     * @return string
309
     */
310
    public function LinkListView()
311
    {
312
        // Note : Force redirect to top level page controller
313
        return $this->LinkWithSearch(CMSMain::singleton()->Link('listview'));
314
    }
315
316 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...
317
    {
318
        if (!$id) {
319
            $id = $this->currentPageID();
320
        }
321
        return $this->LinkWithSearch(
322
            Controller::join_links(CMSPageEditController::singleton()->Link('show'), $id)
323
        );
324
    }
325
326 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...
327
    {
328
        if ($id = $this->currentPageID()) {
329
            return $this->LinkWithSearch(
330
                Controller::join_links(CMSPageSettingsController::singleton()->Link('show'), $id)
331
            );
332
        } else {
333
            return null;
334
        }
335
    }
336
337 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...
338
    {
339
        if ($id = $this->currentPageID()) {
340
            return $this->LinkWithSearch(
341
                Controller::join_links(CMSPageHistoryController::singleton()->Link('show'), $id)
342
            );
343
        } else {
344
            return null;
345
        }
346
    }
347
348
    public function LinkWithSearch($link)
349
    {
350
        // Whitelist to avoid side effects
351
        $params = array(
352
            'q' => (array)$this->getRequest()->getVar('q'),
353
            'ParentID' => $this->getRequest()->getVar('ParentID')
354
        );
355
        $link = Controller::join_links(
356
            $link,
357
            array_filter(array_values($params)) ? '?' . http_build_query($params) : null
358
        );
359
        $this->extend('updateLinkWithSearch', $link);
360
        return $link;
361
    }
362
363
    public function LinkPageAdd($extra = null, $placeholders = null)
364
    {
365
        $link = CMSPageAddController::singleton()->Link();
366
        $this->extend('updateLinkPageAdd', $link);
367
368
        if ($extra) {
369
            $link = Controller::join_links($link, $extra);
370
        }
371
372
        if ($placeholders) {
373
            $link .= (strpos($link, '?') === false ? "?$placeholders" : "&$placeholders");
374
        }
375
376
        return $link;
377
    }
378
379
    /**
380
     * @return string
381
     */
382
    public function LinkPreview()
383
    {
384
        $record = $this->getRecord($this->currentPageID());
385
        $baseLink = Director::absoluteBaseURL();
386
        if ($record && $record instanceof SiteTree) {
387
            // if we are an external redirector don't show a link
388
            if ($record instanceof RedirectorPage && $record->RedirectionType == 'External') {
389
                $baseLink = false;
390
            } else {
391
                $baseLink = $record->Link('?stage=Stage');
392
            }
393
        }
394
        return $baseLink;
395
    }
396
397
    /**
398
     * Return the entire site tree as a nested set of ULs
399
     */
400
    public function SiteTreeAsUL()
401
    {
402
        // Pre-cache sitetree version numbers for querying efficiency
403
        Versioned::prepopulate_versionnumber_cache(SiteTree::class, Versioned::DRAFT);
404
        Versioned::prepopulate_versionnumber_cache(SiteTree::class, Versioned::LIVE);
405
        $html = $this->getSiteTreeFor($this->stat('tree_class'));
406
407
        $this->extend('updateSiteTreeAsUL', $html);
408
409
        return $html;
410
    }
411
412
    /**
413
     * Get a site tree HTML listing which displays the nodes under the given criteria.
414
     *
415
     * @param string $className The class of the root object
416
     * @param string $rootID The ID of the root object.  If this is null then a complete tree will be
417
     *  shown
418
     * @param string $childrenMethod The method to call to get the children of the tree. For example,
419
     *  Children, AllChildrenIncludingDeleted, or AllHistoricalChildren
420
     * @param string $numChildrenMethod
421
     * @param callable $filterFunction
422
     * @param int $nodeCountThreshold
423
     * @return string Nested unordered list with links to each page
424
     */
425
    public function getSiteTreeFor(
426
        $className,
427
        $rootID = null,
428
        $childrenMethod = null,
429
        $numChildrenMethod = null,
430
        $filterFunction = null,
431
        $nodeCountThreshold = 30
432
    ) {
433
        // Provide better defaults from filter
434
        $filter = $this->getSearchFilter();
435
        if ($filter) {
436
            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...
437
                $childrenMethod = $filter->getChildrenMethod();
438
            }
439
            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...
440
                $numChildrenMethod = $filter->getNumChildrenMethod();
441
            }
442
            if (!$filterFunction) {
443
                $filterFunction = function ($node) use ($filter) {
444
                    return $filter->isPageIncluded($node);
445
                };
446
            }
447
        }
448
449
        // Build set from node and begin marking
450
        $record = ($rootID) ? $this->getRecord($rootID) : null;
451
        $rootNode = $record ? $record : DataObject::singleton($className);
452
        $markingSet = MarkedSet::create($rootNode, $childrenMethod, $numChildrenMethod, $nodeCountThreshold);
453
454
        // Set filter function
455
        if ($filterFunction) {
456
            $markingSet->setMarkingFilterFunction($filterFunction);
457
        }
458
459
        // Mark tree from this node
460
        $markingSet->markPartialTree();
461
462
        // Ensure current page is exposed
463
        $currentPage = $this->currentPage();
464
        if ($currentPage) {
465
            $markingSet->markToExpose($currentPage);
466
        }
467
468
        // Pre-cache permissions
469
        SiteTree::prepopulate_permission_cache(
470
            'CanEditType',
471
            $markingSet->markedNodeIDs(),
472
            [ SiteTree::class, 'can_edit_multiple']
473
        );
474
475
        // Render using full-subtree template
476
        return $markingSet->renderChildren(
477
            [ 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...
478
            $this->getTreeNodeCustomisations()
479
        );
480
    }
481
482
483
    /**
484
     * Get callback to determine template customisations for nodes
485
     *
486
     * @return callable
487
     */
488
    protected function getTreeNodeCustomisations()
489
    {
490
        $rootTitle = $this->getCMSTreeTitle();
491
        $linkWithSearch = $this->LinkWithSearch($this->Link());
492
        return function (SiteTree $node) use ($linkWithSearch, $rootTitle) {
493
            return [
494
                'listViewLink' => Controller::join_links(
495
                    $linkWithSearch,
496
                    '?view=listview&ParentID=' . $node->ID
497
                ),
498
                'rootTitle' => $rootTitle,
499
                'extraClass' => $this->getTreeNodeClasses($node),
500
            ];
501
        };
502
    }
503
504
    /**
505
     * Get extra CSS classes for a page's tree node
506
     *
507
     * @param SiteTree $node
508
     * @return string
509
     */
510
    public function getTreeNodeClasses(SiteTree $node)
511
    {
512
        // Get classes from object
513
        $classes = $node->CMSTreeClasses();
514
515
        // Flag as current
516
        if ($this->isCurrentPage($node)) {
517
            $classes .= ' current';
518
        }
519
520
        // Get status flag classes
521
        $flags = $node->getStatusFlags();
522
        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...
523
            $statuses = array_keys($flags);
524
            foreach ($statuses as $s) {
525
                $classes .= ' status-' . $s;
526
            }
527
        }
528
529
        // Get additional filter classes
530
        $filter = $this->getSearchFilter();
531
        if ($filter && ($filterClasses = $filter->getPageClasses($node))) {
532
            if (is_array($filterClasses)) {
533
                $filterClasses = implode(' ', $filterClasses);
534
            }
535
            $classes .= ' ' . $filterClasses;
536
        }
537
538
        return trim($classes);
539
    }
540
541
    /**
542
     * Get a subtree underneath the request param 'ID'.
543
     * If ID = 0, then get the whole tree.
544
     *
545
     * @param HTTPRequest $request
546
     * @return string
547
     */
548
    public function getsubtree($request)
549
    {
550
        $html = $this->getSiteTreeFor(
551
            $this->stat('tree_class'),
552
            $request->getVar('ID'),
553
            null,
554
            null,
555
            null,
556
            $request->getVar('minNodeCount')
557
        );
558
559
        // Trim off the outer tag
560
        $html = preg_replace('/^[\s\t\r\n]*<ul[^>]*>/', '', $html);
561
        $html = preg_replace('/<\/ul[^>]*>[\s\t\r\n]*$/', '', $html);
562
563
        return $html;
564
    }
565
566
    /**
567
     * Allows requesting a view update on specific tree nodes.
568
     * Similar to {@link getsubtree()}, but doesn't enforce loading
569
     * all children with the node. Useful to refresh views after
570
     * state modifications, e.g. saving a form.
571
     *
572
     * @param HTTPRequest $request
573
     * @return HTTPResponse
574
     */
575
    public function updatetreenodes($request)
576
    {
577
        $data = array();
578
        $ids = explode(',', $request->getVar('ids'));
579
        foreach ($ids as $id) {
580
            if ($id === "") {
581
                continue; // $id may be a blank string, which is invalid and should be skipped over
582
            }
583
584
            $record = $this->getRecord($id);
585
            if (!$record) {
586
                continue; // In case a page is no longer available
587
            }
588
589
            // Create marking set with sole marked root
590
            $markingSet = MarkedSet::create($record);
591
            $markingSet->setMarkingFilterFunction(function () {
592
                return false;
593
            });
594
            $markingSet->markUnexpanded($record);
595
596
            // Find the next & previous nodes, for proper positioning (Sort isn't good enough - it's not a raw offset)
597
            // TODO: These methods should really be in hierarchy - for a start it assumes Sort exists
598
            $prev = null;
599
600
            $className = $this->stat('tree_class');
601
            $next = DataObject::get($className)
602
                ->filter('ParentID', $record->ParentID)
603
                ->filter('Sort:GreaterThan', $record->Sort)
604
                ->first();
605
606
            if (!$next) {
607
                $prev = DataObject::get($className)
608
                    ->filter('ParentID', $record->ParentID)
609
                    ->filter('Sort:LessThan', $record->Sort)
610
                    ->reverse()
611
                    ->first();
612
            }
613
614
            // Render using single node template
615
            $html = $markingSet->renderChildren(
616
                [ 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...
617
                $this->getTreeNodeCustomisations()
618
            );
619
620
            $data[$id] = array(
621
                'html' => $html,
622
                'ParentID' => $record->ParentID,
623
                'NextID' => $next ? $next->ID : null,
624
                'PrevID' => $prev ? $prev->ID : null
625
            );
626
        }
627
        return $this
628
            ->getResponse()
629
            ->addHeader('Content-Type', 'application/json')
630
            ->setBody(Convert::raw2json($data));
631
    }
632
633
    /**
634
     * Update the position and parent of a tree node.
635
     * Only saves the node if changes were made.
636
     *
637
     * Required data:
638
     * - 'ID': The moved node
639
     * - 'ParentID': New parent relation of the moved node (0 for root)
640
     * - 'SiblingIDs': Array of all sibling nodes to the moved node (incl. the node itself).
641
     *   In case of a 'ParentID' change, relates to the new siblings under the new parent.
642
     *
643
     * @param HTTPRequest $request
644
     * @return HTTPResponse JSON string with a
645
     * @throws HTTPResponse_Exception
646
     */
647
    public function savetreenode($request)
648
    {
649
        if (!SecurityToken::inst()->checkRequest($request)) {
650
            return $this->httpError(400);
651
        }
652
        if (!Permission::check('SITETREE_REORGANISE') && !Permission::check('ADMIN')) {
653
            return $this->httpError(
654
                403,
655
                _t(
656
                    __CLASS__.'.CANT_REORGANISE',
657
                    "You do not have permission to rearange the site tree. Your change was not saved."
658
                )
659
            );
660
        }
661
662
        $className = $this->stat('tree_class');
663
        $id = $request->requestVar('ID');
664
        $parentID = $request->requestVar('ParentID');
665
        if (!is_numeric($id) || !is_numeric($parentID)) {
666
            return $this->httpError(400);
667
        }
668
669
        // Check record exists in the DB
670
        /** @var SiteTree $node */
671
        $node = DataObject::get_by_id($className, $id);
672
        if (!$node) {
673
            return $this->httpError(
674
                500,
675
                _t(
676
                    __CLASS__.'.PLEASESAVE',
677
                    "Please Save Page: This page could not be updated because it hasn't been saved yet."
678
                )
679
            );
680
        }
681
682
        // Check top level permissions
683
        $root = $node->getParentType();
684
        if (($parentID == '0' || $root == 'root') && !SiteConfig::current_site_config()->canCreateTopLevel()) {
685
            return $this->httpError(
686
                403,
687
                _t(
688
                    __CLASS__.'.CANT_REORGANISE',
689
                    "You do not have permission to alter Top level pages. Your change was not saved."
690
                )
691
            );
692
        }
693
694
        $siblingIDs = $request->requestVar('SiblingIDs');
695
        $statusUpdates = array('modified'=>array());
696
697
        if (!$node->canEdit()) {
698
            return Security::permissionFailure($this);
699
        }
700
701
        // Update hierarchy (only if ParentID changed)
702
        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...
703
            $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...
704
            $node->write();
705
706
            $statusUpdates['modified'][$node->ID] = array(
707
                '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...
708
            );
709
710
            // Update all dependent pages
711
            $virtualPages = VirtualPage::get()->filter("CopyContentFromID", $node->ID);
712
            foreach ($virtualPages as $virtualPage) {
713
                $statusUpdates['modified'][$virtualPage->ID] = array(
714
                    'TreeTitle' => $virtualPage->TreeTitle()
715
                );
716
            }
717
718
            $this->getResponse()->addHeader(
719
                'X-Status',
720
                rawurlencode(_t(__CLASS__.'.REORGANISATIONSUCCESSFUL', 'Reorganised the site tree successfully.'))
721
            );
722
        }
723
724
        // Update sorting
725
        if (is_array($siblingIDs)) {
726
            $counter = 0;
727
            foreach ($siblingIDs as $id) {
728
                if ($id == $node->ID) {
729
                    $node->Sort = ++$counter;
730
                    $node->write();
731
                    $statusUpdates['modified'][$node->ID] = array(
732
                        '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...
733
                    );
734
                } elseif (is_numeric($id)) {
735
                    // Nodes that weren't "actually moved" shouldn't be registered as
736
                    // having been edited; do a direct SQL update instead
737
                    ++$counter;
738
                    $table = DataObject::getSchema()->baseDataTable($className);
739
                    DB::prepared_query(
740
                        "UPDATE \"$table\" SET \"Sort\" = ? WHERE \"ID\" = ?",
741
                        array($counter, $id)
742
                    );
743
                }
744
            }
745
746
            $this->getResponse()->addHeader(
747
                'X-Status',
748
                rawurlencode(_t(__CLASS__.'.REORGANISATIONSUCCESSFUL', 'Reorganised the site tree successfully.'))
749
            );
750
        }
751
752
        return $this
753
            ->getResponse()
754
            ->addHeader('Content-Type', 'application/json')
755
            ->setBody(Convert::raw2json($statusUpdates));
756
    }
757
758
    public function CanOrganiseSitetree()
759
    {
760
        return !Permission::check('SITETREE_REORGANISE') && !Permission::check('ADMIN') ? false : true;
761
    }
762
763
    /**
764
     * @return boolean
765
     */
766
    public function TreeIsFiltered()
767
    {
768
        $query = $this->getRequest()->getVar('q');
769
770
        if (!$query || (count($query) === 1 && isset($query['FilterClass']) && $query['FilterClass'] === 'SilverStripe\\CMS\\Controllers\\CMSSiteTreeFilter_Search')) {
771
            return false;
772
        }
773
774
        return true;
775
    }
776
777
    public function ExtraTreeTools()
778
    {
779
        $html = '';
780
        $this->extend('updateExtraTreeTools', $html);
781
        return $html;
782
    }
783
784
    /**
785
     * Returns a Form for page searching for use in templates.
786
     *
787
     * Can be modified from a decorator by a 'updateSearchForm' method
788
     *
789
     * @return Form
790
     */
791
    public function SearchForm()
792
    {
793
        // Create the fields
794
        $content = new TextField('q[Term]', _t('SilverStripe\\CMS\\Search\\SearchForm.FILTERLABELTEXT', 'Search'));
795
        $dateFrom = new DateField(
796
            'q[LastEditedFrom]',
797
            _t('SilverStripe\\CMS\\Search\\SearchForm.FILTERDATEFROM', 'From')
798
        );
799
        $dateTo = new DateField(
800
            'q[LastEditedTo]',
801
            _t('SilverStripe\\CMS\\Search\\SearchForm.FILTERDATETO', 'To')
802
        );
803
        $pageFilter = new DropdownField(
804
            'q[FilterClass]',
805
            _t('SilverStripe\\CMS\\Controllers\\CMSMain.PAGES', 'Page status'),
806
            CMSSiteTreeFilter::get_all_filters()
807
        );
808
        $pageClasses = new DropdownField(
809
            'q[ClassName]',
810
            _t('SilverStripe\\CMS\\Controllers\\CMSMain.PAGETYPEOPT', 'Page type', 'Dropdown for limiting search to a page type'),
811
            $this->getPageTypes()
812
        );
813
        $pageClasses->setEmptyString(_t('SilverStripe\\CMS\\Controllers\\CMSMain.PAGETYPEANYOPT', 'Any'));
814
815
        // Group the Datefields
816
        $dateGroup = new FieldGroup(
817
            $dateFrom,
818
            $dateTo
819
        );
820
        $dateGroup->setTitle(_t('SilverStripe\\CMS\\Search\\SearchForm.PAGEFILTERDATEHEADING', 'Last edited'));
821
822
        // view mode
823
        $viewMode = HiddenField::create('view', false, $this->ViewState());
824
825
        // Create the Field list
826
        $fields = new FieldList(
827
            $content,
828
            $pageFilter,
829
            $pageClasses,
830
            $dateGroup,
831
            $viewMode
832
        );
833
834
        // Create the Search and Reset action
835
        $actions = new FieldList(
836
            FormAction::create('doSearch', _t('SilverStripe\\CMS\\Controllers\\CMSMain.APPLY_FILTER', 'Search'))
837
                ->addExtraClass('btn btn-primary'),
838
            FormAction::create('clear', _t('SilverStripe\\CMS\\Controllers\\CMSMain.CLEAR_FILTER', 'Clear'))
839
                ->setAttribute('type', 'reset')
840
                ->addExtraClass('btn btn-secondary')
841
        );
842
843
        // Use <button> to allow full jQuery UI styling on the all of the Actions
844
        /** @var FormAction $action */
845
        foreach ($actions->dataFields() as $action) {
846
            /** @var FormAction $action */
847
            $action->setUseButtonTag(true);
848
        }
849
850
        // Create the form
851
        /** @skipUpgrade */
852
        $form = Form::create($this, 'SearchForm', $fields, $actions)
853
            ->addExtraClass('cms-search-form')
854
            ->setFormMethod('GET')
855
            ->setFormAction($this->Link())
856
            ->disableSecurityToken()
857
            ->unsetValidator();
858
859
        // Load the form with previously sent search data
860
        $form->loadDataFrom($this->getRequest()->getVars());
861
862
        // Allow decorators to modify the form
863
        $this->extend('updateSearchForm', $form);
864
865
        return $form;
866
    }
867
868
    /**
869
     * Returns a sorted array suitable for a dropdown with pagetypes and their translated name
870
     *
871
     * @return array
872
     */
873
    protected function getPageTypes()
874
    {
875
        $pageTypes = array();
876
        foreach (SiteTree::page_type_classes() as $pageTypeClass) {
877
            $pageTypes[$pageTypeClass] = SiteTree::singleton($pageTypeClass)->i18n_singular_name();
878
        }
879
        asort($pageTypes);
880
        return $pageTypes;
881
    }
882
883
    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...
884
    {
885
        return $this->getsubtree($this->getRequest());
886
    }
887
888
    /**
889
     * @param bool $unlinked
890
     * @return ArrayList
891
     */
892
    public function Breadcrumbs($unlinked = false)
893
    {
894
        $items = parent::Breadcrumbs($unlinked);
895
896
        if ($items->count() > 1) {
897
            // Specific to the SiteTree admin section, we never show the cms section and current
898
            // page in the same breadcrumbs block.
899
            $items->shift();
900
        }
901
902
        return $items;
903
    }
904
905
    /**
906
     * Create serialized JSON string with site tree hints data to be injected into
907
     * 'data-hints' attribute of root node of jsTree.
908
     *
909
     * @return string Serialized JSON
910
     */
911
    public function SiteTreeHints()
912
    {
913
        $classes = SiteTree::page_type_classes();
914
915
        $cacheCanCreate = array();
916
        foreach ($classes as $class) {
917
            $cacheCanCreate[$class] = singleton($class)->canCreate();
918
        }
919
920
        // Generate basic cache key. Too complex to encompass all variations
921
        $cache = Injector::inst()->get(CacheInterface::class . '.CMSMain_SiteTreeHints');
922
        $cacheKey = md5(implode('_', array(Member::currentUserID(), implode(',', $cacheCanCreate), implode(',', $classes))));
923
        if ($this->getRequest()->getVar('flush')) {
924
            $cache->clear();
925
        }
926
        $json = $cache->get($cacheKey);
927
        if (!$json) {
928
            $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...
929
            $def['Root']['disallowedChildren'] = array();
930
931
            // Contains all possible classes to support UI controls listing them all,
932
            // such as the "add page here" context menu.
933
            $def['All'] = array();
934
935
            // Identify disallows and set globals
936
            foreach ($classes as $class) {
937
                $obj = singleton($class);
938
                if ($obj instanceof HiddenClass) {
939
                    continue;
940
                }
941
942
                // Name item
943
                $def['All'][$class] = array(
944
                    'title' => $obj->i18n_singular_name()
945
                );
946
947
                // Check if can be created at the root
948
                $needsPerm = $obj->stat('need_permission');
949
                if (!$obj->stat('can_be_root')
950
                    || (!array_key_exists($class, $cacheCanCreate) || !$cacheCanCreate[$class])
951
                    || ($needsPerm && !$this->can($needsPerm))
952
                ) {
953
                    $def['Root']['disallowedChildren'][] = $class;
954
                }
955
956
                // Hint data specific to the class
957
                $def[$class] = array();
958
959
                $defaultChild = $obj->defaultChild();
960
                if ($defaultChild !== 'Page' && $defaultChild !== null) {
961
                    $def[$class]['defaultChild'] = $defaultChild;
962
                }
963
964
                $defaultParent = $obj->defaultParent();
965
                if ($defaultParent !== 1 && $defaultParent !== null) {
966
                    $def[$class]['defaultParent'] = $defaultParent;
967
                }
968
            }
969
970
            $this->extend('updateSiteTreeHints', $def);
971
972
            $json = Convert::raw2json($def);
973
            $cache->set($cacheKey, $json);
974
        }
975
        return $json;
976
    }
977
978
    /**
979
     * Populates an array of classes in the CMS
980
     * which allows the user to change the page type.
981
     *
982
     * @return SS_List
983
     */
984
    public function PageTypes()
985
    {
986
        $classes = SiteTree::page_type_classes();
987
988
        $result = new ArrayList();
989
990
        foreach ($classes as $class) {
991
            $instance = SiteTree::singleton($class);
992
            if ($instance instanceof HiddenClass) {
993
                continue;
994
            }
995
996
            // skip this type if it is restricted
997
            if ($instance->stat('need_permission') && !$this->can(singleton($class)->stat('need_permission'))) {
998
                continue;
999
            }
1000
1001
            $singularName = $instance->i18n_singular_name();
1002
            $description = $instance->i18n_classDescription();
1003
1004
            $result->push(new ArrayData(array(
1005
                'ClassName' => $class,
1006
                'AddAction' => $singularName,
1007
                'Description' => $description,
1008
                // TODO Sprite support
1009
                'IconURL' => $instance->stat('icon'),
1010
                'Title' => $singularName,
1011
            )));
1012
        }
1013
1014
        $result = $result->sort('AddAction');
1015
1016
        return $result;
1017
    }
1018
1019
    /**
1020
     * Get a database record to be managed by the CMS.
1021
     *
1022
     * @param int $id Record ID
1023
     * @param int $versionID optional Version id of the given record
1024
     * @return SiteTree
1025
     */
1026
    public function getRecord($id, $versionID = null)
1027
    {
1028
        if (!$id) {
1029
            return null;
1030
        }
1031
        $treeClass = $this->stat('tree_class');
1032
        if ($id instanceof $treeClass) {
1033
            return $id;
1034
        }
1035
        if (substr($id, 0, 3) == 'new') {
1036
            return $this->getNewItem($id);
1037
        }
1038
        if (!is_numeric($id)) {
1039
            return null;
1040
        }
1041
1042
        $currentStage = Versioned::get_reading_mode();
1043
1044
        if ($this->getRequest()->getVar('Version')) {
1045
            $versionID = (int) $this->getRequest()->getVar('Version');
1046
        }
1047
1048
        /** @var SiteTree $record */
1049
        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...
1050
            $record = Versioned::get_version($treeClass, $id, $versionID);
1051
        } else {
1052
            $record = DataObject::get_by_id($treeClass, $id);
1053
        }
1054
1055
        // Then, try getting a record from the live site
1056
        if (!$record) {
1057
            // $record = Versioned::get_one_by_stage($treeClass, "Live", "\"$treeClass\".\"ID\" = $id");
1058
            Versioned::set_stage(Versioned::LIVE);
1059
            singleton($treeClass)->flushCache();
1060
1061
            $record = DataObject::get_by_id($treeClass, $id);
1062
        }
1063
1064
        // Then, try getting a deleted record
1065
        if (!$record) {
1066
            $record = Versioned::get_latest_version($treeClass, $id);
1067
        }
1068
1069
        // Set the reading mode back to what it was.
1070
        Versioned::set_reading_mode($currentStage);
1071
1072
        return $record;
1073
    }
1074
1075
    /**
1076
     * {@inheritdoc}
1077
     *
1078
     * @param HTTPRequest $request
1079
     * @return Form
1080
     */
1081
    public function EditForm($request = null)
1082
    {
1083
        // set page ID from request
1084
        if ($request) {
1085
            // Validate id is present
1086
            $id = $request->param('ID');
1087
            if (!isset($id)) {
1088
                $this->httpError(400);
1089
                return null;
1090
            }
1091
            $this->setCurrentPageID($id);
1092
        }
1093
        return $this->getEditForm();
1094
    }
1095
1096
    /**
1097
     * @param int $id
1098
     * @param FieldList $fields
1099
     * @return Form
1100
     */
1101
    public function getEditForm($id = null, $fields = null)
1102
    {
1103
        // Get record
1104
        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...
1105
            $id = $this->currentPageID();
1106
        }
1107
        /** @var SiteTree $record */
1108
        $record = $this->getRecord($id);
1109
1110
        // Check parent form can be generated
1111
        $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...
1112
        if (!$form || !$record) {
1113
            return $form;
1114
        }
1115
1116
        if (!$fields) {
1117
            $fields = $form->Fields();
1118
        }
1119
1120
        // Add extra fields
1121
        $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...
1122
        $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...
1123
        // Necessary for different subsites
1124
        $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...
1125
        $fields->push($liveLinkField = new HiddenField("LiveLink"));
1126
        $fields->push($stageLinkField = new HiddenField("StageLink"));
1127
        $fields->push($archiveWarningMsgField = new HiddenField("ArchiveWarningMessage"));
1128
        $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...
1129
1130
        $archiveWarningMsgField->setValue($this->getArchiveWarningMessage($record));
1131
1132
        // Build preview / live links
1133
        $liveLink = $record->getAbsoluteLiveLink();
1134
        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...
1135
            $liveLinkField->setValue($liveLink);
1136
        }
1137
        if (!$deletedFromStage) {
1138
            $stageLink = Controller::join_links($record->AbsoluteLink(), '?stage=Stage');
1139
            if ($stageLink) {
1140
                $stageLinkField->setValue($stageLink);
1141
            }
1142
        }
1143
1144
        // Added in-line to the form, but plucked into different view by LeftAndMain.Preview.js upon load
1145
        /** @skipUpgrade */
1146
        if ($record instanceof CMSPreviewable && !$fields->fieldByName('SilverStripeNavigator')) {
1147
            $navField = new LiteralField('SilverStripeNavigator', $this->getSilverStripeNavigator());
1148
            $navField->setAllowHTML(true);
1149
            $fields->push($navField);
1150
        }
1151
1152
        // getAllCMSActions can be used to completely redefine the action list
1153
        if ($record->hasMethod('getAllCMSActions')) {
1154
            $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...
1155
        } else {
1156
            $actions = $record->getCMSActions();
1157
1158
            // Find and remove action menus that have no actions.
1159
            if ($actions && $actions->count()) {
1160
                /** @var TabSet $tabset */
1161
                $tabset = $actions->fieldByName('ActionMenus');
1162
                if ($tabset) {
1163
                    /** @var Tab $tab */
1164
                    foreach ($tabset->getChildren() as $tab) {
1165
                        if (!$tab->getChildren()->count()) {
1166
                            $tabset->removeByName($tab->getName());
1167
                        }
1168
                    }
1169
                }
1170
            }
1171
        }
1172
1173
        // Use <button> to allow full jQuery UI styling
1174
        $actionsFlattened = $actions->dataFields();
1175
        if ($actionsFlattened) {
1176
            /** @var FormAction $action */
1177
            foreach ($actionsFlattened as $action) {
1178
                $action->setUseButtonTag(true);
1179
            }
1180
        }
1181
1182
        // TODO Can't merge $FormAttributes in template at the moment
1183
        $form->addExtraClass('center ' . $this->BaseCSSClasses());
1184
        // Set validation exemptions for specific actions
1185
        $form->setValidationExemptActions(array('restore', 'revert', 'deletefromlive', 'delete', 'unpublish', 'rollback', 'doRollback'));
1186
1187
        // Announce the capability so the frontend can decide whether to allow preview or not.
1188
        if ($record instanceof CMSPreviewable) {
1189
            $form->addExtraClass('cms-previewable');
1190
        }
1191
        $form->addExtraClass('fill-height flexbox-area-grow');
1192
1193
        if (!$record->canEdit() || $deletedFromStage) {
1194
            $readonlyFields = $form->Fields()->makeReadonly();
1195
            $form->setFields($readonlyFields);
1196
        }
1197
1198
        $form->Fields()->setForm($form);
1199
1200
        $this->extend('updateEditForm', $form);
1201
1202
        // Use custom reqest handler for LeftAndMain requests;
1203
        // CMS Forms cannot be identified solely by name, but also need ID (and sometimes OtherID)
1204
        $form->setRequestHandler(
1205
            LeftAndMainFormRequestHandler::create($form, [$id])
1206
        );
1207
        return $form;
1208
    }
1209
1210
    public function EmptyForm()
1211
    {
1212
        $fields = new FieldList(
1213
            new LabelField('PageDoesntExistLabel', _t('SilverStripe\\CMS\\Controllers\\CMSMain.PAGENOTEXISTS', "This page doesn't exist"))
1214
        );
1215
        $form = parent::EmptyForm();
1216
        $form->setFields($fields);
1217
        $fields->setForm($form);
1218
        return $form;
1219
    }
1220
1221
    /**
1222
     * Build an archive warning message based on the page's children
1223
     *
1224
     * @param SiteTree $record
1225
     * @return string
1226
     */
1227
    protected function getArchiveWarningMessage($record)
1228
    {
1229
        // Get all page's descendants
1230
        $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...
1231
        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...
1232
            $descendants = [];
1233
        }
1234
1235
        // Get all campaigns that the page and its descendants belong to
1236
        $inChangeSetIDs = ChangeSetItem::get_for_object($record)->column('ChangeSetID');
1237
1238
        foreach ($descendants as $page) {
1239
            $inChangeSetIDs = array_merge($inChangeSetIDs, ChangeSetItem::get_for_object($page)->column('ChangeSetID'));
1240
        }
1241
1242
        if (count($inChangeSetIDs) > 0) {
1243
            $inChangeSets = ChangeSet::get()->filter(['ID' => $inChangeSetIDs, 'State' => ChangeSet::STATE_OPEN]);
1244
        } else {
1245
            $inChangeSets = new ArrayList();
1246
        }
1247
1248
        $numCampaigns = ChangeSet::singleton()->i18n_pluralise($inChangeSets->count());
1249
        $numCampaigns = mb_strtolower($numCampaigns);
1250
1251
        if (count($descendants) > 0 && $inChangeSets->count() > 0) {
1252
            $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 ]);
1253
        } elseif (count($descendants) > 0) {
1254
            $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?');
1255
        } elseif ($inChangeSets->count() > 0) {
1256
            $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 ]);
1257
        } else {
1258
            $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?');
1259
        }
1260
1261
        return $archiveWarningMsg;
1262
    }
1263
1264
    /**
1265
     * @param HTTPRequest $request
1266
     * @return string HTML
1267
     */
1268
    public function treeview($request)
1269
    {
1270
        return $this->getResponseNegotiator()->respond($request);
1271
    }
1272
1273
    /**
1274
     * @param HTTPRequest $request
1275
     * @return string HTML
1276
     */
1277
    public function listview($request)
1278
    {
1279
        return $this->getResponseNegotiator()->respond($request);
1280
    }
1281
1282
    /**
1283
     * @return string
1284
     */
1285
    public function ViewState()
1286
    {
1287
        $mode = $this->getRequest()->requestVar('view')
1288
            ?: $this->getRequest()->param('Action');
1289
        switch ($mode) {
1290
            case 'listview':
1291
            case 'treeview':
1292
                return $mode;
1293
            default:
1294
                return 'treeview';
1295
        }
1296
    }
1297
1298
    /**
1299
     * Callback to request the list of page types allowed under a given page instance.
1300
     * Provides a slower but more precise response over SiteTreeHints
1301
     *
1302
     * @param HTTPRequest $request
1303
     * @return HTTPResponse
1304
     */
1305
    public function childfilter($request)
1306
    {
1307
        // Check valid parent specified
1308
        $parentID = $request->requestVar('ParentID');
1309
        $parent = SiteTree::get()->byID($parentID);
1310
        if (!$parent || !$parent->exists()) {
1311
            return $this->httpError(404);
1312
        }
1313
1314
        // Build hints specific to this class
1315
        // Identify disallows and set globals
1316
        $classes = SiteTree::page_type_classes();
1317
        $disallowedChildren = array();
1318
        foreach ($classes as $class) {
1319
            $obj = singleton($class);
1320
            if ($obj instanceof HiddenClass) {
1321
                continue;
1322
            }
1323
1324
            if (!$obj->canCreate(null, array('Parent' => $parent))) {
1325
                $disallowedChildren[] = $class;
1326
            }
1327
        }
1328
1329
        $this->extend('updateChildFilter', $disallowedChildren, $parentID);
1330
        return $this
1331
            ->getResponse()
1332
            ->addHeader('Content-Type', 'application/json; charset=utf-8')
1333
            ->setBody(Convert::raw2json($disallowedChildren));
1334
    }
1335
1336
    /**
1337
     * Safely reconstruct a selected filter from a given set of query parameters
1338
     *
1339
     * @param array $params Query parameters to use
1340
     * @return CMSSiteTreeFilter The filter class, or null if none present
1341
     * @throws InvalidArgumentException if invalid filter class is passed.
1342
     */
1343
    protected function getQueryFilter($params)
1344
    {
1345
        if (empty($params['FilterClass'])) {
1346
            return null;
1347
        }
1348
        $filterClass = $params['FilterClass'];
1349
        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...
1350
            throw new InvalidArgumentException("Invalid filter class passed: {$filterClass}");
1351
        }
1352
        return $filterClass::create($params);
1353
    }
1354
1355
    /**
1356
     * Returns the pages meet a certain criteria as {@see CMSSiteTreeFilter} or the subpages of a parent page
1357
     * defaulting to no filter and show all pages in first level.
1358
     * Doubles as search results, if any search parameters are set through {@link SearchForm()}.
1359
     *
1360
     * @param array $params Search filter criteria
1361
     * @param int $parentID Optional parent node to filter on (can't be combined with other search criteria)
1362
     * @return SS_List
1363
     * @throws InvalidArgumentException if invalid filter class is passed.
1364
     */
1365
    public function getList($params = array(), $parentID = 0)
1366
    {
1367
        if ($filter = $this->getQueryFilter($params)) {
1368
            return $filter->getFilteredPages();
1369
        } else {
1370
            $list = DataList::create($this->stat('tree_class'));
1371
            $parentID = is_numeric($parentID) ? $parentID : 0;
1372
            return $list->filter("ParentID", $parentID);
1373
        }
1374
    }
1375
1376
    /**
1377
     * @return Form
1378
     */
1379
    public function ListViewForm()
1380
    {
1381
        $params = $this->getRequest()->requestVar('q');
1382
        $list = $this->getList($params, $parentID = $this->getRequest()->requestVar('ParentID'));
1383
        $gridFieldConfig = GridFieldConfig::create()->addComponents(
1384
            new GridFieldSortableHeader(),
1385
            new GridFieldDataColumns(),
1386
            new GridFieldPaginator($this->config()->get('page_length'))
1387
        );
1388
        if ($parentID) {
1389
            $linkSpec = $this->Link();
1390
            $linkSpec = $linkSpec . (strstr($linkSpec, '?') ? '&' : '?') . 'ParentID=%d&view=listview';
1391
            $gridFieldConfig->addComponent(
1392
                GridFieldLevelup::create($parentID)
1393
                    ->setLinkSpec($linkSpec)
1394
                    ->setAttributes(array('data-pjax' => 'ListViewForm,Breadcrumbs'))
1395
            );
1396
        }
1397
        $gridField = new GridField('Page', 'Pages', $list, $gridFieldConfig);
1398
        /** @var GridFieldDataColumns $columns */
1399
        $columns = $gridField->getConfig()->getComponentByType('SilverStripe\\Forms\\GridField\\GridFieldDataColumns');
1400
1401
        // Don't allow navigating into children nodes on filtered lists
1402
        $fields = array(
1403
            'getTreeTitle' => _t('SilverStripe\\CMS\\Model\\SiteTree.PAGETITLE', 'Page Title'),
1404
            'singular_name' => _t('SilverStripe\\CMS\\Model\\SiteTree.PAGETYPE', 'Page Type'),
1405
            'LastEdited' => _t('SilverStripe\\CMS\\Model\\SiteTree.LASTUPDATED', 'Last Updated'),
1406
        );
1407
        /** @var GridFieldSortableHeader $sortableHeader */
1408
        $sortableHeader = $gridField->getConfig()->getComponentByType('SilverStripe\\Forms\\GridField\\GridFieldSortableHeader');
1409
        $sortableHeader->setFieldSorting(array('getTreeTitle' => 'Title'));
1410
        $gridField->getState()->ParentID = $parentID;
1411
1412
        if (!$params) {
1413
            $fields = array_merge(array('listChildrenLink' => ''), $fields);
1414
        }
1415
1416
        $columns->setDisplayFields($fields);
1417
        $columns->setFieldCasting(array(
1418
            'Created' => 'DBDatetime->Ago',
1419
            'LastEdited' => 'DBDatetime->FormatFromSettings',
1420
            'getTreeTitle' => 'HTMLFragment'
1421
        ));
1422
1423
        $controller = $this;
1424
        $columns->setFieldFormatting(array(
1425
            'listChildrenLink' => function ($value, &$item) use ($controller) {
1426
                /** @var SiteTree $item */
1427
                $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...
1428
                if ($num) {
1429
                    return sprintf(
1430
                        '<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>',
1431
                        Controller::join_links(
1432
                            $controller->Link(),
1433
                            sprintf("?ParentID=%d&view=listview", (int)$item->ID)
1434
                        ),
1435
                        $num
1436
                    );
1437
                }
1438
            },
1439
            'getTreeTitle' => function ($value, &$item) use ($controller) {
1440
                return sprintf(
1441
                    '<a class="action-detail" href="%s">%s</a>',
1442
                    Controller::join_links(
1443
                        CMSPageEditController::singleton()->Link('show'),
1444
                        (int)$item->ID
1445
                    ),
1446
                    $item->TreeTitle // returns HTML, does its own escaping
1447
                );
1448
            }
1449
        ));
1450
1451
        $negotiator = $this->getResponseNegotiator();
1452
        $listview = Form::create(
1453
            $this,
1454
            'ListViewForm',
1455
            new FieldList($gridField),
1456
            new FieldList()
1457
        )->setHTMLID('Form_ListViewForm');
1458
        $listview->setAttribute('data-pjax-fragment', 'ListViewForm');
1459 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...
1460
            $request = $this->getRequest();
1461
            if ($request->isAjax() && $negotiator) {
1462
                $result = $listview->forTemplate();
1463
                return $negotiator->respond($request, array(
1464
                    'CurrentForm' => function () use ($result) {
1465
                        return $result;
1466
                    }
1467
                ));
1468
            }
1469
        });
1470
1471
        $this->extend('updateListView', $listview);
1472
1473
        $listview->disableSecurityToken();
1474
        return $listview;
1475
    }
1476
1477
    public function currentPageID()
1478
    {
1479
        $id = parent::currentPageID();
1480
1481
        $this->extend('updateCurrentPageID', $id);
1482
1483
        return $id;
1484
    }
1485
1486
    //------------------------------------------------------------------------------------------//
1487
    // Data saving handlers
1488
1489
    /**
1490
     * Save and Publish page handler
1491
     *
1492
     * @param array $data
1493
     * @param Form $form
1494
     * @return HTTPResponse
1495
     * @throws HTTPResponse_Exception
1496
     */
1497
    public function save($data, $form)
1498
    {
1499
        $className = $this->stat('tree_class');
1500
1501
        // Existing or new record?
1502
        $id = $data['ID'];
1503
        if (substr($id, 0, 3) != 'new') {
1504
            /** @var SiteTree $record */
1505
            $record = DataObject::get_by_id($className, $id);
1506
            // Check edit permissions
1507
            if ($record && !$record->canEdit()) {
1508
                return Security::permissionFailure($this);
1509
            }
1510
            if (!$record || !$record->ID) {
1511
                throw new HTTPResponse_Exception("Bad record ID #$id", 404);
1512
            }
1513
        } else {
1514
            if (!$className::singleton()->canCreate()) {
1515
                return Security::permissionFailure($this);
1516
            }
1517
            $record = $this->getNewItem($id, false);
1518
        }
1519
1520
        // Check publishing permissions
1521
        $doPublish = !empty($data['publish']);
1522
        if ($record && $doPublish && !$record->canPublish()) {
1523
            return Security::permissionFailure($this);
1524
        }
1525
1526
        // TODO Coupling to SiteTree
1527
        $record->HasBrokenLink = 0;
1528
        $record->HasBrokenFile = 0;
1529
1530
        if (!$record->ObsoleteClassName) {
1531
            $record->writeWithoutVersion();
1532
        }
1533
1534
        // Update the class instance if necessary
1535
        if (isset($data['ClassName']) && $data['ClassName'] != $record->ClassName) {
1536
            // Replace $record with a new instance of the new class
1537
            $newClassName = $data['ClassName'];
1538
            $record = $record->newClassInstance($newClassName);
1539
        }
1540
1541
        // save form data into record
1542
        $form->saveInto($record);
1543
        $record->write();
1544
1545
        // If the 'Save & Publish' button was clicked, also publish the page
1546
        if ($doPublish) {
1547
            $record->publishRecursive();
1548
            $message = _t(
1549
                'SilverStripe\\CMS\\Controllers\\CMSMain.PUBLISHED',
1550
                "Published '{title}' successfully.",
1551
                ['title' => $record->Title]
1552
            );
1553
        } else {
1554
            $message = _t(
1555
                'SilverStripe\\CMS\\Controllers\\CMSMain.SAVED',
1556
                "Saved '{title}' successfully.",
1557
                ['title' => $record->Title]
1558
            );
1559
        }
1560
1561
        $this->getResponse()->addHeader('X-Status', rawurlencode($message));
1562
        return $this->getResponseNegotiator()->respond($this->getRequest());
1563
    }
1564
1565
    /**
1566
     * @uses LeftAndMainExtension->augmentNewSiteTreeItem()
1567
     *
1568
     * @param int|string $id
1569
     * @param bool $setID
1570
     * @return mixed|DataObject
1571
     * @throws HTTPResponse_Exception
1572
     */
1573
    public function getNewItem($id, $setID = true)
1574
    {
1575
        $parentClass = $this->stat('tree_class');
1576
        list($dummy, $className, $parentID, $suffix) = array_pad(explode('-', $id), 4, null);
0 ignored issues
show
Unused Code introduced by
The assignment to $dummy is unused. Consider omitting it like so list($first,,$third).

This checks looks for assignemnts to variables using the list(...) function, where not all assigned variables are subsequently used.

Consider the following code example.

<?php

function returnThreeValues() {
    return array('a', 'b', 'c');
}

list($a, $b, $c) = returnThreeValues();

print $a . " - " . $c;

Only the variables $a and $c are used. There was no need to assign $b.

Instead, the list call could have been.

list($a,, $c) = returnThreeValues();
Loading history...
1577
1578
        if (!is_a($className, $parentClass, true)) {
1579
            $response = Security::permissionFailure($this);
1580
            if (!$response) {
1581
                $response = $this->getResponse();
1582
            }
1583
            throw new HTTPResponse_Exception($response);
1584
        }
1585
1586
        /** @var SiteTree $newItem */
1587
        $newItem = Injector::inst()->create($className);
1588
        if (!$suffix) {
1589
            $sessionTag = "NewItems." . $parentID . "." . $className;
1590
            if (Session::get($sessionTag)) {
1591
                $suffix = '-' . Session::get($sessionTag);
1592
                Session::set($sessionTag, Session::get($sessionTag) + 1);
1593
            } else {
1594
                Session::set($sessionTag, 1);
1595
            }
1596
1597
                $id = $id . $suffix;
1598
        }
1599
1600
        $newItem->Title = _t(
1601
            'SilverStripe\\CMS\\Controllers\\CMSMain.NEWPAGE',
1602
            "New {pagetype}",
1603
            'followed by a page type title',
1604
            array('pagetype' => singleton($className)->i18n_singular_name())
1605
        );
1606
        $newItem->ClassName = $className;
1607
        $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...
1608
1609
        // DataObject::fieldExists only checks the current class, not the hierarchy
1610
        // This allows the CMS to set the correct sort value
1611
        if ($newItem->castingHelper('Sort')) {
1612
            $newItem->Sort = DB::prepared_query('SELECT MAX("Sort") FROM "SiteTree" WHERE "ParentID" = ?', array($parentID))->value() + 1;
1613
        }
1614
1615
        if ($setID) {
1616
            $newItem->ID = $id;
1617
        }
1618
1619
        # Some modules like subsites add extra fields that need to be set when the new item is created
1620
        $this->extend('augmentNewSiteTreeItem', $newItem);
1621
1622
        return $newItem;
1623
    }
1624
1625
    /**
1626
     * Actually perform the publication step
1627
     *
1628
     * @param Versioned|DataObject $record
1629
     * @return mixed
1630
     */
1631
    public function performPublish($record)
1632
    {
1633
        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...
1634
            return Security::permissionFailure($this);
1635
        }
1636
1637
        $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...
1638
    }
1639
1640
    /**
1641
     * Reverts a page by publishing it to live.
1642
     * Use {@link restorepage()} if you want to restore a page
1643
     * which was deleted from draft without publishing.
1644
     *
1645
     * @uses SiteTree->doRevertToLive()
1646
     *
1647
     * @param array $data
1648
     * @param Form $form
1649
     * @return HTTPResponse
1650
     * @throws HTTPResponse_Exception
1651
     */
1652
    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...
1653
    {
1654
        if (!isset($data['ID'])) {
1655
            throw new HTTPResponse_Exception("Please pass an ID in the form content", 400);
1656
        }
1657
1658
        $id = (int) $data['ID'];
1659
        $restoredPage = Versioned::get_latest_version(SiteTree::class, $id);
1660
        if (!$restoredPage) {
1661
            throw new HTTPResponse_Exception("SiteTree #$id not found", 400);
1662
        }
1663
1664
        /** @var SiteTree $record */
1665
        $record = Versioned::get_one_by_stage('SilverStripe\\CMS\\Model\\SiteTree', 'Live', array(
1666
            '"SiteTree_Live"."ID"' => $id
1667
        ));
1668
1669
        // a user can restore a page without publication rights, as it just adds a new draft state
1670
        // (this action should just be available when page has been "deleted from draft")
1671
        if ($record && !$record->canEdit()) {
1672
            return Security::permissionFailure($this);
1673
        }
1674
        if (!$record || !$record->ID) {
1675
            throw new HTTPResponse_Exception("Bad record ID #$id", 404);
1676
        }
1677
1678
        $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...
1679
1680
        $this->getResponse()->addHeader(
1681
            'X-Status',
1682
            rawurlencode(_t(
1683
                'SilverStripe\\CMS\\Controllers\\CMSMain.RESTORED',
1684
                "Restored '{title}' successfully",
1685
                'Param %s is a title',
1686
                array('title' => $record->Title)
1687
            ))
1688
        );
1689
1690
        return $this->getResponseNegotiator()->respond($this->getRequest());
1691
    }
1692
1693
    /**
1694
     * Delete the current page from draft stage.
1695
     *
1696
     * @see deletefromlive()
1697
     *
1698
     * @param array $data
1699
     * @param Form $form
1700
     * @return HTTPResponse
1701
     * @throws HTTPResponse_Exception
1702
     */
1703 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...
1704
    {
1705
        $id = $data['ID'];
1706
        $record = SiteTree::get()->byID($id);
1707
        if ($record && !$record->canDelete()) {
1708
            return Security::permissionFailure();
1709
        }
1710
        if (!$record || !$record->ID) {
1711
            throw new HTTPResponse_Exception("Bad record ID #$id", 404);
1712
        }
1713
1714
        // Delete record
1715
        $record->delete();
1716
1717
        $this->getResponse()->addHeader(
1718
            'X-Status',
1719
            rawurlencode(sprintf(_t('SilverStripe\\CMS\\Controllers\\CMSMain.REMOVEDPAGEFROMDRAFT', "Removed '%s' from the draft site"), $record->Title))
1720
        );
1721
1722
        // Even if the record has been deleted from stage and live, it can be viewed in "archive mode"
1723
        return $this->getResponseNegotiator()->respond($this->getRequest());
1724
    }
1725
1726
    /**
1727
     * Delete this page from both live and stage
1728
     *
1729
     * @param array $data
1730
     * @param Form $form
1731
     * @return HTTPResponse
1732
     * @throws HTTPResponse_Exception
1733
     */
1734 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...
1735
    {
1736
        $id = $data['ID'];
1737
        /** @var SiteTree $record */
1738
        $record = SiteTree::get()->byID($id);
1739
        if (!$record || !$record->exists()) {
1740
            throw new HTTPResponse_Exception("Bad record ID #$id", 404);
1741
        }
1742
        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...
1743
            return Security::permissionFailure();
1744
        }
1745
1746
        // Archive record
1747
        $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...
1748
1749
        $this->getResponse()->addHeader(
1750
            'X-Status',
1751
            rawurlencode(sprintf(_t('SilverStripe\\CMS\\Controllers\\CMSMain.ARCHIVEDPAGE', "Archived page '%s'"), $record->Title))
1752
        );
1753
1754
        // Even if the record has been deleted from stage and live, it can be viewed in "archive mode"
1755
        return $this->getResponseNegotiator()->respond($this->getRequest());
1756
    }
1757
1758
    public function publish($data, $form)
1759
    {
1760
        $data['publish'] = '1';
1761
1762
        return $this->save($data, $form);
1763
    }
1764
1765
    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...
1766
    {
1767
        $className = $this->stat('tree_class');
1768
        /** @var SiteTree $record */
1769
        $record = DataObject::get_by_id($className, $data['ID']);
1770
1771
        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...
1772
            return Security::permissionFailure($this);
1773
        }
1774
        if (!$record || !$record->ID) {
1775
            throw new HTTPResponse_Exception("Bad record ID #" . (int)$data['ID'], 404);
1776
        }
1777
1778
        $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...
1779
1780
        $this->getResponse()->addHeader(
1781
            'X-Status',
1782
            rawurlencode(_t('SilverStripe\\CMS\\Controllers\\CMSMain.REMOVEDPAGE', "Removed '{title}' from the published site", array('title' => $record->Title)))
1783
        );
1784
1785
        return $this->getResponseNegotiator()->respond($this->getRequest());
1786
    }
1787
1788
    /**
1789
     * @return HTTPResponse
1790
     */
1791
    public function rollback()
1792
    {
1793
        return $this->doRollback(array(
1794
            'ID' => $this->currentPageID(),
1795
            'Version' => $this->getRequest()->param('VersionID')
1796
        ), 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...
1797
    }
1798
1799
    /**
1800
     * Rolls a site back to a given version ID
1801
     *
1802
     * @param array $data
1803
     * @param Form $form
1804
     * @return HTTPResponse
1805
     */
1806
    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...
1807
    {
1808
        $this->extend('onBeforeRollback', $data['ID']);
1809
1810
        $id = (isset($data['ID'])) ? (int) $data['ID'] : null;
1811
        $version = (isset($data['Version'])) ? (int) $data['Version'] : null;
1812
1813
        /** @var DataObject|Versioned $record */
1814
        $record = DataObject::get_by_id($this->stat('tree_class'), $id);
1815
        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...
1816
            return Security::permissionFailure($this);
1817
        }
1818
1819
        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...
1820
            $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...
1821
            $message = _t(
1822
                'SilverStripe\\CMS\\Controllers\\CMSMain.ROLLEDBACKVERSIONv2',
1823
                "Rolled back to version #%d.",
1824
                array('version' => $data['Version'])
1825
            );
1826
        } else {
1827
            $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...
1828
            $message = _t(
1829
                'SilverStripe\\CMS\\Controllers\\CMSMain.ROLLEDBACKPUBv2',
1830
                "Rolled back to published version."
1831
            );
1832
        }
1833
1834
        $this->getResponse()->addHeader('X-Status', rawurlencode($message));
1835
1836
        // Can be used in different contexts: In normal page edit view, in which case the redirect won't have any effect.
1837
        // Or in history view, in which case a revert causes the CMS to re-load the edit view.
1838
        // The X-Pjax header forces a "full" content refresh on redirect.
1839
        $url = Controller::join_links(CMSPageEditController::singleton()->Link('show'), $record->ID);
1840
        $this->getResponse()->addHeader('X-ControllerURL', $url);
1841
        $this->getRequest()->addHeader('X-Pjax', 'Content');
1842
        $this->getResponse()->addHeader('X-Pjax', 'Content');
1843
1844
        return $this->getResponseNegotiator()->respond($this->getRequest());
1845
    }
1846
1847
    /**
1848
     * Batch Actions Handler
1849
     */
1850
    public function batchactions()
1851
    {
1852
        return new CMSBatchActionHandler($this, 'batchactions');
1853
    }
1854
1855
    public function BatchActionParameters()
1856
    {
1857
        $batchActions = CMSBatchActionHandler::config()->batch_actions;
1858
1859
        $forms = array();
1860
        foreach ($batchActions as $urlSegment => $batchAction) {
1861
            $SNG_action = singleton($batchAction);
1862
            if ($SNG_action->canView() && $fieldset = $SNG_action->getParameterFields()) {
1863
                $formHtml = '';
1864
                /** @var FormField $field */
1865
                foreach ($fieldset as $field) {
1866
                    $formHtml .= $field->Field();
1867
                }
1868
                $forms[$urlSegment] = $formHtml;
1869
            }
1870
        }
1871
        $pageHtml = '';
1872
        foreach ($forms as $urlSegment => $html) {
1873
            $pageHtml .= "<div class=\"params\" id=\"BatchActionParameters_$urlSegment\">$html</div>\n\n";
1874
        }
1875
        return new LiteralField("BatchActionParameters", '<div id="BatchActionParameters" style="display:none">'.$pageHtml.'</div>');
1876
    }
1877
    /**
1878
     * Returns a list of batch actions
1879
     */
1880
    public function BatchActionList()
1881
    {
1882
        return $this->batchactions()->batchActionList();
1883
    }
1884
1885
    public function publishall($request)
1886
    {
1887
        if (!Permission::check('ADMIN')) {
1888
            return Security::permissionFailure($this);
1889
        }
1890
1891
        increase_time_limit_to();
1892
        increase_memory_limit_to();
1893
1894
        $response = "";
1895
1896
        if (isset($this->requestParams['confirm'])) {
1897
            // Protect against CSRF on destructive action
1898
            if (!SecurityToken::inst()->checkRequest($request)) {
1899
                return $this->httpError(400);
1900
            }
1901
1902
            $start = 0;
1903
            $pages = SiteTree::get()->limit("$start,30");
1904
            $count = 0;
1905
            while ($pages) {
1906
                /** @var SiteTree $page */
1907
                foreach ($pages as $page) {
1908
                    if ($page && !$page->canPublish()) {
1909
                        return Security::permissionFailure($this);
1910
                    }
1911
1912
                    $page->publishRecursive();
1913
                    $page->destroy();
1914
                    unset($page);
1915
                    $count++;
1916
                    $response .= "<li>$count</li>";
1917
                }
1918
                if ($pages->count() > 29) {
1919
                    $start += 30;
1920
                    $pages = SiteTree::get()->limit("$start,30");
1921
                } else {
1922
                    break;
1923
                }
1924
            }
1925
            $response .= _t('SilverStripe\\CMS\\Controllers\\CMSMain.PUBPAGES', "Done: Published {count} pages", array('count' => $count));
1926
        } else {
1927
            $token = SecurityToken::inst();
1928
            $fields = new FieldList();
1929
            $token->updateFieldSet($fields);
1930
            $tokenField = $fields->first();
1931
            $tokenHtml = ($tokenField) ? $tokenField->FieldHolder() : '';
1932
            $publishAllDescription = _t(
1933
                'SilverStripe\\CMS\\Controllers\\CMSMain.PUBALLFUN2',
1934
                'Pressing this button will do the equivalent of going to every page and pressing "publish".  '
1935
                . 'It\'s intended to be used after there have been massive edits of the content, such as when '
1936
                . 'the site was first built.'
1937
            );
1938
            $response .= '<h1>' . _t('SilverStripe\\CMS\\Controllers\\CMSMain.PUBALLFUN', '"Publish All" functionality') . '</h1>
1939
				<p>' . $publishAllDescription . '</p>
1940
				<form method="post" action="publishall">
1941
					<input type="submit" name="confirm" value="'
1942
                    . _t('SilverStripe\\CMS\\Controllers\\CMSMain.PUBALLCONFIRM', "Please publish every page in the site, copying content stage to live", 'Confirmation button') .'" />'
1943
                    . $tokenHtml .
1944
                '</form>';
1945
        }
1946
1947
        return $response;
1948
    }
1949
1950
    /**
1951
     * Restore a completely deleted page from the SiteTree_versions table.
1952
     *
1953
     * @param array $data
1954
     * @param Form $form
1955
     * @return HTTPResponse
1956
     */
1957
    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...
1958
    {
1959
        if (!isset($data['ID']) || !is_numeric($data['ID'])) {
1960
            return new HTTPResponse("Please pass an ID in the form content", 400);
1961
        }
1962
1963
        $id = (int)$data['ID'];
1964
        /** @var SiteTree $restoredPage */
1965
        $restoredPage = Versioned::get_latest_version(SiteTree::class, $id);
1966
        if (!$restoredPage) {
1967
            return new HTTPResponse("SiteTree #$id not found", 400);
1968
        }
1969
1970
        $restoredPage = $restoredPage->doRestoreToStage();
1971
1972
        $this->getResponse()->addHeader(
1973
            'X-Status',
1974
            rawurlencode(_t(
1975
                'SilverStripe\\CMS\\Controllers\\CMSMain.RESTORED',
1976
                "Restored '{title}' successfully",
1977
                array('title' => $restoredPage->Title)
1978
            ))
1979
        );
1980
1981
        return $this->getResponseNegotiator()->respond($this->getRequest());
1982
    }
1983
1984
    public function duplicate($request)
1985
    {
1986
        // Protect against CSRF on destructive action
1987
        if (!SecurityToken::inst()->checkRequest($request)) {
1988
            return $this->httpError(400);
1989
        }
1990
1991
        if (($id = $this->urlParams['ID']) && is_numeric($id)) {
1992
            /** @var SiteTree $page */
1993
            $page = SiteTree::get()->byID($id);
1994 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...
1995
                return Security::permissionFailure($this);
1996
            }
1997
            if (!$page || !$page->ID) {
1998
                throw new HTTPResponse_Exception("Bad record ID #$id", 404);
1999
            }
2000
2001
            $newPage = $page->duplicate();
2002
2003
            // ParentID can be hard-set in the URL.  This is useful for pages with multiple parents
2004
            if (isset($_GET['parentID']) && is_numeric($_GET['parentID'])) {
2005
                $newPage->ParentID = $_GET['parentID'];
2006
                $newPage->write();
2007
            }
2008
2009
            $this->getResponse()->addHeader(
2010
                'X-Status',
2011
                rawurlencode(_t(
2012
                    'SilverStripe\\CMS\\Controllers\\CMSMain.DUPLICATED',
2013
                    "Duplicated '{title}' successfully",
2014
                    array('title' => $newPage->Title)
2015
                ))
2016
            );
2017
            $url = Controller::join_links(CMSPageEditController::singleton()->Link('show'), $newPage->ID);
2018
            $this->getResponse()->addHeader('X-ControllerURL', $url);
2019
            $this->getRequest()->addHeader('X-Pjax', 'Content');
2020
            $this->getResponse()->addHeader('X-Pjax', 'Content');
2021
2022
            return $this->getResponseNegotiator()->respond($this->getRequest());
2023
        } else {
2024
            return new HTTPResponse("CMSMain::duplicate() Bad ID: '$id'", 400);
2025
        }
2026
    }
2027
2028
    public function duplicatewithchildren($request)
2029
    {
2030
        // Protect against CSRF on destructive action
2031
        if (!SecurityToken::inst()->checkRequest($request)) {
2032
            return $this->httpError(400);
2033
        }
2034
        increase_time_limit_to();
2035
        if (($id = $this->urlParams['ID']) && is_numeric($id)) {
2036
            /** @var SiteTree $page */
2037
            $page = SiteTree::get()->byID($id);
2038 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...
2039
                return Security::permissionFailure($this);
2040
            }
2041
            if (!$page || !$page->ID) {
2042
                throw new HTTPResponse_Exception("Bad record ID #$id", 404);
2043
            }
2044
2045
            $newPage = $page->duplicateWithChildren();
2046
2047
            $this->getResponse()->addHeader(
2048
                'X-Status',
2049
                rawurlencode(_t(
2050
                    'SilverStripe\\CMS\\Controllers\\CMSMain.DUPLICATEDWITHCHILDREN',
2051
                    "Duplicated '{title}' and children successfully",
2052
                    array('title' => $newPage->Title)
2053
                ))
2054
            );
2055
            $url = Controller::join_links(CMSPageEditController::singleton()->Link('show'), $newPage->ID);
2056
            $this->getResponse()->addHeader('X-ControllerURL', $url);
2057
            $this->getRequest()->addHeader('X-Pjax', 'Content');
2058
            $this->getResponse()->addHeader('X-Pjax', 'Content');
2059
2060
            return $this->getResponseNegotiator()->respond($this->getRequest());
2061
        } else {
2062
            return new HTTPResponse("CMSMain::duplicatewithchildren() Bad ID: '$id'", 400);
2063
        }
2064
    }
2065
2066
    public function providePermissions()
2067
    {
2068
        $title = CMSPagesController::menu_title();
2069
        return array(
2070
            "CMS_ACCESS_CMSMain" => array(
2071
                'name' => _t('SilverStripe\\CMS\\Controllers\\CMSMain.ACCESS', "Access to '{title}' section", array('title' => $title)),
2072
                'category' => _t('SilverStripe\\Security\\Permission.CMS_ACCESS_CATEGORY', 'CMS Access'),
2073
                'help' => _t(
2074
                    'SilverStripe\\CMS\\Controllers\\CMSMain.ACCESS_HELP',
2075
                    '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".'
2076
                ),
2077
                'sort' => -99 // below "CMS_ACCESS_LeftAndMain", but above everything else
2078
            )
2079
        );
2080
    }
2081
2082
    /**
2083
     * Get title for root CMS node
2084
     *
2085
     * @return string
2086
     */
2087
    protected function getCMSTreeTitle()
2088
    {
2089
        $rootTitle = SiteConfig::current_site_config()->Title;
2090
        $this->extend('updateCMSTreeTitle', $rootTitle);
2091
        return $rootTitle;
2092
    }
2093
}
2094