Completed
Pull Request — master (#1854)
by Damian
02:57
created

CMSMain::LinkPageHistory()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 10
Code Lines 6

Duplication

Lines 9
Ratio 90 %

Importance

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