Completed
Push — master ( cab21e...ed90cc )
by Daniel
02:43
created

CMSMain::Link()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 11
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 11
rs 9.4285
cc 1
eloc 8
nc 1
nop 1
1
<?php
2
3
namespace SilverStripe\CMS\Controllers;
4
5
use SilverStripe\Admin\AdminRootController;
6
use SilverStripe\Admin\CMSBatchActionHandler;
7
use SilverStripe\Admin\LeftAndMain_SearchFilter;
8
use SilverStripe\Admin\LeftAndMainFormRequestHandler;
9
use SilverStripe\CMS\Model\VirtualPage;
10
use SilverStripe\Core\Environment;
11
use SilverStripe\Forms\Tab;
12
use SilverStripe\ORM\CMSPreviewable;
13
use SilverStripe\Admin\LeftAndMain;
14
use SilverStripe\CMS\BatchActions\CMSBatchAction_Archive;
15
use SilverStripe\CMS\BatchActions\CMSBatchAction_Publish;
16
use SilverStripe\CMS\BatchActions\CMSBatchAction_Restore;
17
use SilverStripe\CMS\BatchActions\CMSBatchAction_Unpublish;
18
use SilverStripe\CMS\Model\CurrentPageIdentifier;
19
use SilverStripe\CMS\Model\RedirectorPage;
20
use SilverStripe\CMS\Model\SiteTree;
21
use SilverStripe\Control\Controller;
22
use SilverStripe\Control\Director;
23
use SilverStripe\Control\Session;
24
use SilverStripe\Control\HTTPRequest;
25
use SilverStripe\Control\HTTPResponse;
26
use SilverStripe\Control\HTTPResponse_Exception;
27
use SilverStripe\Core\Convert;
28
use SilverStripe\Core\Injector\Injector;
29
use SilverStripe\Core\Manifest\ModuleLoader;
30
use Psr\SimpleCache\CacheInterface;
31
use SilverStripe\Forms\DateField;
32
use SilverStripe\Forms\DropdownField;
33
use SilverStripe\Forms\FieldGroup;
34
use SilverStripe\Forms\FieldList;
35
use SilverStripe\Forms\Form;
36
use SilverStripe\Forms\FormAction;
37
use SilverStripe\Forms\FormField;
38
use SilverStripe\Forms\GridField\GridField;
39
use SilverStripe\Forms\GridField\GridFieldConfig;
40
use SilverStripe\Forms\GridField\GridFieldDataColumns;
41
use SilverStripe\Forms\GridField\GridFieldLevelup;
42
use SilverStripe\Forms\GridField\GridFieldPaginator;
43
use SilverStripe\Forms\GridField\GridFieldSortableHeader;
44
use SilverStripe\Forms\HiddenField;
45
use SilverStripe\Forms\LabelField;
46
use SilverStripe\Forms\LiteralField;
47
use SilverStripe\Forms\TabSet;
48
use SilverStripe\Forms\TextField;
49
use SilverStripe\ORM\ArrayList;
50
use SilverStripe\ORM\DataList;
51
use SilverStripe\ORM\DataObject;
52
use SilverStripe\ORM\DB;
53
use SilverStripe\ORM\FieldType\DBHTMLText;
54
use SilverStripe\ORM\HiddenClass;
55
use SilverStripe\ORM\Hierarchy\MarkedSet;
56
use SilverStripe\ORM\SS_List;
57
use SilverStripe\ORM\ValidationResult;
58
use SilverStripe\Security\InheritedPermissions;
59
use SilverStripe\SiteConfig\SiteConfig;
60
use SilverStripe\Versioned\Versioned;
61
use SilverStripe\Security\Member;
62
use SilverStripe\Security\Permission;
63
use SilverStripe\Security\PermissionProvider;
64
use SilverStripe\Security\Security;
65
use SilverStripe\Security\SecurityToken;
66
use SilverStripe\View\ArrayData;
67
use SilverStripe\View\Requirements;
68
use Translatable;
69
use InvalidArgumentException;
70
use SilverStripe\Versioned\ChangeSet;
71
use SilverStripe\Versioned\ChangeSetItem;
72
73
/**
74
 * The main "content" area of the CMS.
75
 *
76
 * This class creates a 2-frame layout - left-tree and right-form - to sit beneath the main
77
 * admin menu.
78
 *
79
 * @todo Create some base classes to contain the generic functionality that will be replicated.
80
 *
81
 * @mixin LeftAndMainPageIconsExtension
82
 */
83
class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionProvider
84
{
85
86
    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...
87
88
    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...
89
90
    // Maintain a lower priority than other administration sections
91
    // so that Director does not think they are actions of CMSMain
92
    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...
93
94
    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...
95
96
    private static $menu_icon_class = 'font-icon-sitemap';
97
98
    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...
99
100
    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...
101
102
    private static $subitem_class = Member::class;
103
104
    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...
105
106
    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...
107
108
    /**
109
     * Amount of results showing on a single page.
110
     *
111
     * @config
112
     * @var int
113
     */
114
    private static $page_length = 15;
115
116
    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...
117
        'archive',
118
        'deleteitems',
119
        'DeleteItemsForm',
120
        'dialog',
121
        'duplicate',
122
        'duplicatewithchildren',
123
        'publishall',
124
        'publishitems',
125
        'PublishItemsForm',
126
        'submit',
127
        'EditForm',
128
        'SearchForm',
129
        'SiteTreeAsUL',
130
        'getshowdeletedsubtree',
131
        'savetreenode',
132
        'getsubtree',
133
        'updatetreenodes',
134
        'batchactions',
135
        'treeview',
136
        'listview',
137
        'ListViewForm',
138
        'childfilter',
139
    );
140
141
    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...
142
        'EditForm/$ID' => 'EditForm',
143
    ];
144
145
    private static $casting = array(
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
146
        'TreeIsFiltered' => 'Boolean',
147
        'AddForm' => 'HTMLFragment',
148
        'LinkPages' => 'Text',
149
        'Link' => 'Text',
150
        'ListViewForm' => 'HTMLFragment',
151
        'ExtraTreeTools' => 'HTMLFragment',
152
        'PageList' => 'HTMLFragment',
153
        'PageListSidebar' => 'HTMLFragment',
154
        'SiteTreeHints' => 'HTMLFragment',
155
        'SecurityID' => 'Text',
156
        'SiteTreeAsUL' => 'HTMLFragment',
157
    );
158
159
    protected function init()
160
    {
161
        // set reading lang
162
        if (SiteTree::has_extension('Translatable') && !$this->getRequest()->isAjax()) {
163
            Translatable::choose_site_locale(array_keys(Translatable::get_existing_content_languages('SilverStripe\\CMS\\Model\\SiteTree')));
164
        }
165
166
        parent::init();
167
168
        Requirements::javascript('silverstripe/cms: client/dist/js/bundle.js');
169
        Requirements::javascript('silverstripe/cms: client/dist/js/SilverStripeNavigator.js');
170
        Requirements::css('silverstripe/cms: client/dist/styles/bundle.css');
171
        Requirements::customCSS($this->generatePageIconsCss());
0 ignored issues
show
Documentation Bug introduced by
The method generatePageIconsCss does not exist on object<SilverStripe\CMS\Controllers\CMSMain>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

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