Completed
Push — master ( d14e00...ecbd77 )
by Ingo
21s
created

CMSMain::getNewItem()   B

Complexity

Conditions 6
Paths 6

Size

Total Lines 43
Code Lines 26

Duplication

Lines 0
Ratio 0 %

Importance

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

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

<?php

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

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

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

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

}

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

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

See also the PhpDoc documentation for @property.

Loading history...
2002
                $newPage->write();
2003
            }
2004
2005
            $this->getResponse()->addHeader(
2006
                'X-Status',
2007
                rawurlencode(_t(
2008
                    'SilverStripe\\CMS\\Controllers\\CMSMain.DUPLICATED',
2009
                    "Duplicated '{title}' successfully",
2010
                    array('title' => $newPage->Title)
2011
                ))
2012
            );
2013
            $url = Controller::join_links(CMSPageEditController::singleton()->Link('show'), $newPage->ID);
2014
            $this->getResponse()->addHeader('X-ControllerURL', $url);
2015
            $this->getRequest()->addHeader('X-Pjax', 'Content');
2016
            $this->getResponse()->addHeader('X-Pjax', 'Content');
2017
2018
            return $this->getResponseNegotiator()->respond($this->getRequest());
2019
        } else {
2020
            return new HTTPResponse("CMSMain::duplicate() Bad ID: '$id'", 400);
2021
        }
2022
    }
2023
2024
    public function duplicatewithchildren($request)
2025
    {
2026
        // Protect against CSRF on destructive action
2027
        if (!SecurityToken::inst()->checkRequest($request)) {
2028
            return $this->httpError(400);
2029
        }
2030
        Environment::increaseTimeLimitTo();
2031
        if (($id = $this->urlParams['ID']) && is_numeric($id)) {
2032
            /** @var SiteTree $page */
2033
            $page = SiteTree::get()->byID($id);
2034 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...
2035
                return Security::permissionFailure($this);
2036
            }
2037
            if (!$page || !$page->ID) {
2038
                throw new HTTPResponse_Exception("Bad record ID #$id", 404);
2039
            }
2040
2041
            $newPage = $page->duplicateWithChildren();
2042
2043
            $this->getResponse()->addHeader(
2044
                'X-Status',
2045
                rawurlencode(_t(
2046
                    'SilverStripe\\CMS\\Controllers\\CMSMain.DUPLICATEDWITHCHILDREN',
2047
                    "Duplicated '{title}' and children successfully",
2048
                    array('title' => $newPage->Title)
2049
                ))
2050
            );
2051
            $url = Controller::join_links(CMSPageEditController::singleton()->Link('show'), $newPage->ID);
2052
            $this->getResponse()->addHeader('X-ControllerURL', $url);
2053
            $this->getRequest()->addHeader('X-Pjax', 'Content');
2054
            $this->getResponse()->addHeader('X-Pjax', 'Content');
2055
2056
            return $this->getResponseNegotiator()->respond($this->getRequest());
2057
        } else {
2058
            return new HTTPResponse("CMSMain::duplicatewithchildren() Bad ID: '$id'", 400);
2059
        }
2060
    }
2061
2062
    public function providePermissions()
2063
    {
2064
        $title = CMSPagesController::menu_title();
2065
        return array(
2066
            "CMS_ACCESS_CMSMain" => array(
2067
                'name' => _t('SilverStripe\\CMS\\Controllers\\CMSMain.ACCESS', "Access to '{title}' section", array('title' => $title)),
2068
                'category' => _t('SilverStripe\\Security\\Permission.CMS_ACCESS_CATEGORY', 'CMS Access'),
2069
                'help' => _t(
2070
                    'SilverStripe\\CMS\\Controllers\\CMSMain.ACCESS_HELP',
2071
                    '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".'
2072
                ),
2073
                'sort' => -99 // below "CMS_ACCESS_LeftAndMain", but above everything else
2074
            )
2075
        );
2076
    }
2077
2078
    /**
2079
     * Get title for root CMS node
2080
     *
2081
     * @return string
2082
     */
2083
    protected function getCMSTreeTitle()
2084
    {
2085
        $rootTitle = SiteConfig::current_site_config()->Title;
2086
        $this->extend('updateCMSTreeTitle', $rootTitle);
2087
        return $rootTitle;
2088
    }
2089
}
2090