Completed
Push — master ( 904f18...8fc070 )
by Ingo
12s
created

CMSMain::getEditForm()   F

Complexity

Conditions 21
Paths 1538

Size

Total Lines 108
Code Lines 55

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 108
rs 2
c 0
b 0
f 0
cc 21
eloc 55
nc 1538
nop 2

How to fix   Long Method    Complexity   

Long Method

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

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

Commonly applied refactorings include:

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

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

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

class ParentClass {
    private $data = array();

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

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

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
170
        Requirements::add_i18n_javascript(CMS_DIR . '/client/lang', false, true);
171
172
        CMSBatchActionHandler::register('restore', CMSBatchAction_Restore::class);
173
        CMSBatchActionHandler::register('archive', CMSBatchAction_Archive::class);
174
        CMSBatchActionHandler::register('unpublish', CMSBatchAction_Unpublish::class);
175
        CMSBatchActionHandler::register('publish', CMSBatchAction_Publish::class);
176
    }
177
178
    public function index($request)
179
    {
180
        // In case we're not showing a specific record, explicitly remove any session state,
181
        // to avoid it being highlighted in the tree, and causing an edit form to show.
182
        if (!$request->param('Action')) {
183
            $this->setCurrentPageID(null);
184
        }
185
186
        return parent::index($request);
187
    }
188
189
    public function getResponseNegotiator()
190
    {
191
        $negotiator = parent::getResponseNegotiator();
192
193
        // ListViewForm
194
        $negotiator->setCallback('ListViewForm', function () {
195
            return $this->ListViewForm()->forTemplate();
196
        });
197
198
        // PageList view
199
        $negotiator->setCallback('Content-PageList', function () {
200
            return $this->PageList()->forTemplate();
201
        });
202
203
        // PageList view for edit controller
204
        $negotiator->setCallback('Content-PageList-Sidebar', function () {
205
            return $this->PageListSidebar()->forTemplate();
206
        });
207
208
        return $negotiator;
209
    }
210
211
    /**
212
     * Get pages listing area
213
     *
214
     * @return DBHTMLText
215
     */
216
    public function PageList()
217
    {
218
        return $this->renderWith($this->getTemplatesWithSuffix('_PageList'));
219
    }
220
221
    /**
222
     * Page list view for edit-form
223
     *
224
     * @return DBHTMLText
225
     */
226
    public function PageListSidebar()
227
    {
228
        return $this->renderWith($this->getTemplatesWithSuffix('_PageList_Sidebar'));
229
    }
230
231
    /**
232
     * If this is set to true, the "switchView" context in the
233
     * template is shown, with links to the staging and publish site.
234
     *
235
     * @return boolean
236
     */
237
    public function ShowSwitchView()
238
    {
239
        return true;
240
    }
241
242
    /**
243
     * Overloads the LeftAndMain::ShowView. Allows to pass a page as a parameter, so we are able
244
     * to switch view also for archived versions.
245
     *
246
     * @param SiteTree $page
247
     * @return array
248
     */
249
    public function SwitchView($page = null)
250
    {
251
        if (!$page) {
252
            $page = $this->currentPage();
253
        }
254
255
        if ($page) {
256
            $nav = SilverStripeNavigator::get_for_record($page);
257
            return $nav['items'];
258
        }
259
    }
260
261
    //------------------------------------------------------------------------------------------//
262
    // Main controllers
263
264
    //------------------------------------------------------------------------------------------//
265
    // Main UI components
266
267
    /**
268
     * Override {@link LeftAndMain} Link to allow blank URL segment for CMSMain.
269
     *
270
     * @param string|null $action Action to link to.
271
     * @return string
272
     */
273
    public function Link($action = null)
274
    {
275
        $link = Controller::join_links(
276
            AdminRootController::admin_url(),
277
            $this->stat('url_segment'), // in case we want to change the segment
278
            '/', // trailing slash needed if $action is null!
279
            "$action"
280
        );
281
        $this->extend('updateLink', $link);
282
        return $link;
283
    }
284
285
    public function LinkPages()
286
    {
287
        return CMSPagesController::singleton()->Link();
288
    }
289
290
    public function LinkPagesWithSearch()
291
    {
292
        return $this->LinkWithSearch($this->LinkPages());
293
    }
294
295
    /**
296
     * Get link to tree view
297
     *
298
     * @return string
299
     */
300
    public function LinkTreeView()
301
    {
302
        // Tree view is just default link to main pages section (no /treeview suffix)
303
        return $this->LinkWithSearch(CMSMain::singleton()->Link());
304
    }
305
306
    /**
307
     * Get link to list view
308
     *
309
     * @return string
310
     */
311
    public function LinkListView()
312
    {
313
        // Note : Force redirect to top level page controller
314
        return $this->LinkWithSearch(CMSMain::singleton()->Link('listview'));
315
    }
316
317 View Code Duplication
    public function LinkPageEdit($id = null)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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

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

Loading history...
318
    {
319
        if (!$id) {
320
            $id = $this->currentPageID();
321
        }
322
        return $this->LinkWithSearch(
323
            Controller::join_links(CMSPageEditController::singleton()->Link('show'), $id)
324
        );
325
    }
326
327 View Code Duplication
    public function LinkPageSettings()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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

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

Loading history...
328
    {
329
        if ($id = $this->currentPageID()) {
330
            return $this->LinkWithSearch(
331
                Controller::join_links(CMSPageSettingsController::singleton()->Link('show'), $id)
332
            );
333
        } else {
334
            return null;
335
        }
336
    }
337
338 View Code Duplication
    public function LinkPageHistory()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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

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

Loading history...
339
    {
340
        if ($id = $this->currentPageID()) {
341
            return $this->LinkWithSearch(
342
                Controller::join_links(CMSPageHistoryController::singleton()->Link('show'), $id)
343
            );
344
        } else {
345
            return null;
346
        }
347
    }
348
349
    public function LinkWithSearch($link)
350
    {
351
        // Whitelist to avoid side effects
352
        $params = array(
353
            'q' => (array)$this->getRequest()->getVar('q'),
354
            'ParentID' => $this->getRequest()->getVar('ParentID')
355
        );
356
        $link = Controller::join_links(
357
            $link,
358
            array_filter(array_values($params)) ? '?' . http_build_query($params) : null
359
        );
360
        $this->extend('updateLinkWithSearch', $link);
361
        return $link;
362
    }
363
364
    public function LinkPageAdd($extra = null, $placeholders = null)
365
    {
366
        $link = CMSPageAddController::singleton()->Link();
367
        $this->extend('updateLinkPageAdd', $link);
368
369
        if ($extra) {
370
            $link = Controller::join_links($link, $extra);
371
        }
372
373
        if ($placeholders) {
374
            $link .= (strpos($link, '?') === false ? "?$placeholders" : "&$placeholders");
375
        }
376
377
        return $link;
378
    }
379
380
    /**
381
     * @return string
382
     */
383
    public function LinkPreview()
384
    {
385
        $record = $this->getRecord($this->currentPageID());
386
        $baseLink = Director::absoluteBaseURL();
387
        if ($record && $record instanceof SiteTree) {
388
            // if we are an external redirector don't show a link
389
            if ($record instanceof RedirectorPage && $record->RedirectionType == 'External') {
390
                $baseLink = false;
391
            } else {
392
                $baseLink = $record->Link('?stage=Stage');
393
            }
394
        }
395
        return $baseLink;
396
    }
397
398
    /**
399
     * Return the entire site tree as a nested set of ULs
400
     */
401
    public function SiteTreeAsUL()
402
    {
403
        // Pre-cache sitetree version numbers for querying efficiency
404
        Versioned::prepopulate_versionnumber_cache(SiteTree::class, Versioned::DRAFT);
405
        Versioned::prepopulate_versionnumber_cache(SiteTree::class, Versioned::LIVE);
406
        $html = $this->getSiteTreeFor($this->stat('tree_class'));
407
408
        $this->extend('updateSiteTreeAsUL', $html);
409
410
        return $html;
411
    }
412
413
    /**
414
     * Get a site tree HTML listing which displays the nodes under the given criteria.
415
     *
416
     * @param string $className The class of the root object
417
     * @param string $rootID The ID of the root object.  If this is null then a complete tree will be
418
     *  shown
419
     * @param string $childrenMethod The method to call to get the children of the tree. For example,
420
     *  Children, AllChildrenIncludingDeleted, or AllHistoricalChildren
421
     * @param string $numChildrenMethod
422
     * @param callable $filterFunction
423
     * @param int $nodeCountThreshold
424
     * @return string Nested unordered list with links to each page
425
     */
426
    public function getSiteTreeFor(
427
        $className,
428
        $rootID = null,
429
        $childrenMethod = null,
430
        $numChildrenMethod = null,
431
        $filterFunction = null,
432
        $nodeCountThreshold = 30
433
    ) {
434
        // Provide better defaults from filter
435
        $filter = $this->getSearchFilter();
436
        if ($filter) {
437
        if (!$childrenMethod) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $childrenMethod of type string|null is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

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

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

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

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

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

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

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

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
441
                $numChildrenMethod = $filter->getNumChildrenMethod();
442
            }
443
            if (!$filterFunction) {
444
            $filterFunction = function ($node) use ($filter) {
445
                return $filter->isPageIncluded($node);
446
            };
447
        }
448
        }
449
450
        // Build set from node and begin marking
451
        $record = ($rootID) ? $this->getRecord($rootID) : null;
452
        $rootNode = $record ? $record : DataObject::singleton($className);
453
        $markingSet = MarkedSet::create($rootNode, $childrenMethod, $numChildrenMethod, $nodeCountThreshold);
454
455
        // Set filter function
456
        if ($filterFunction) {
457
            $markingSet->setMarkingFilterFunction($filterFunction);
458
        }
459
460
        // Mark tree from this node
461
        $markingSet->markPartialTree();
462
463
        // Ensure current page is exposed
464
        $currentPage = $this->currentPage();
465
        if ($currentPage) {
466
            $markingSet->markToExpose($currentPage);
467
        }
468
469
        // Pre-cache permissions
470
        SiteTree::prepopulate_permission_cache(
471
            'CanEditType',
472
            $markingSet->markedNodeIDs(),
473
            [ SiteTree::class, 'can_edit_multiple']
474
        );
475
476
        // Render using full-subtree template
477
        return $markingSet->renderChildren(
478
            [ self::class . '_SubTree', 'type' => 'Includes' ],
479
            $this->getTreeNodeCustomisations()
480
        );
481
    }
482
483
484
    /**
485
     * Get callback to determine template customisations for nodes
486
     *
487
     * @return callable
488
     */
489
    protected function getTreeNodeCustomisations()
490
    {
491
        $rootTitle = $this->getCMSTreeTitle();
492
        $linkWithSearch = $this->LinkWithSearch($this->Link());
493
        return function (SiteTree $node) use ($linkWithSearch, $rootTitle) {
494
            return [
495
                'listViewLink' => Controller::join_links(
496
                    $linkWithSearch,
497
                    '?view=listview&ParentID=' . $node->ID
498
                    ),
499
                'rootTitle' => $rootTitle,
500
                'extraClass' => $this->getTreeNodeClasses($node),
501
            ];
502
            };
503
        }
504
505
    /**
506
     * Get extra CSS classes for a page's tree node
507
     *
508
     * @param SiteTree $node
509
     * @return string
510
     */
511
    public function getTreeNodeClasses(SiteTree $node)
512
    {
513
        // Get classes from object
514
        $classes = $node->CMSTreeClasses();
515
516
        // Flag as current
517
        if ($this->isCurrentPage($node)) {
518
            $classes .= ' current';
519
        }
520
521
        // Get status flag classes
522
        $flags = $node->getStatusFlags();
523
        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...
524
            $statuses = array_keys($flags);
525
            foreach ($statuses as $s) {
526
                $classes .= ' status-' . $s;
527
            }
528
        }
529
530
        // Get additional filter classes
531
        $filter = $this->getSearchFilter();
532
        if ($filter && ($filterClasses = $filter->getPageClasses($node))) {
533
            if (is_array($filterClasses)) {
534
                $filterClasses = implode(' ', $filterClasses);
535
            }
536
            $classes .= ' ' . $filterClasses;
537
        }
538
539
        return trim($classes);
540
    }
541
542
    /**
543
     * Get a subtree underneath the request param 'ID'.
544
     * If ID = 0, then get the whole tree.
545
     *
546
     * @param HTTPRequest $request
547
     * @return string
548
     */
549
    public function getsubtree($request)
550
    {
551
        $html = $this->getSiteTreeFor(
552
            $this->stat('tree_class'),
553
            $request->getVar('ID'),
554
            null,
555
            null,
556
            null,
557
            $request->getVar('minNodeCount')
558
        );
559
560
        // Trim off the outer tag
561
        $html = preg_replace('/^[\s\t\r\n]*<ul[^>]*>/', '', $html);
562
        $html = preg_replace('/<\/ul[^>]*>[\s\t\r\n]*$/', '', $html);
563
564
        return $html;
565
    }
566
567
    /**
568
     * Allows requesting a view update on specific tree nodes.
569
     * Similar to {@link getsubtree()}, but doesn't enforce loading
570
     * all children with the node. Useful to refresh views after
571
     * state modifications, e.g. saving a form.
572
     *
573
     * @param HTTPRequest $request
574
     * @return HTTPResponse
575
     */
576
    public function updatetreenodes($request)
577
    {
578
        $data = array();
579
        $ids = explode(',', $request->getVar('ids'));
580
        foreach ($ids as $id) {
581
            if ($id === "") {
582
                continue; // $id may be a blank string, which is invalid and should be skipped over
583
            }
584
585
            $record = $this->getRecord($id);
586
            if (!$record) {
587
                continue; // In case a page is no longer available
588
            }
589
590
            // Create marking set with sole marked root
591
            $markingSet = MarkedSet::create($record);
592
            $markingSet->setMarkingFilterFunction(function () {
593
                return false;
594
            });
595
            $markingSet->markUnexpanded($record);
596
597
            // Find the next & previous nodes, for proper positioning (Sort isn't good enough - it's not a raw offset)
598
            // TODO: These methods should really be in hierarchy - for a start it assumes Sort exists
599
            $prev = null;
600
601
            $className = $this->stat('tree_class');
602
            $next = DataObject::get($className)
603
                ->filter('ParentID', $record->ParentID)
604
                ->filter('Sort:GreaterThan', $record->Sort)
605
                ->first();
606
607
            if (!$next) {
608
                $prev = DataObject::get($className)
609
                    ->filter('ParentID', $record->ParentID)
610
                    ->filter('Sort:LessThan', $record->Sort)
611
                    ->reverse()
612
                    ->first();
613
            }
614
615
            // Render using single node template
616
            $html = $markingSet->renderChildren(
617
                [ self::class . '_TreeNode', 'type' => 'Includes'],
618
                $this->getTreeNodeCustomisations()
619
            );
620
621
            $data[$id] = array(
622
                'html' => $html,
623
                'ParentID' => $record->ParentID,
624
                'NextID' => $next ? $next->ID : null,
625
                'PrevID' => $prev ? $prev->ID : null
626
            );
627
        }
628
        return $this
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $this->getRespons...vert::raw2json($data)); (SilverStripe\Control\HTTPResponse) is incompatible with the return type of the parent method SilverStripe\Admin\LeftAndMain::updatetreenodes of type string.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
629
            ->getResponse()
630
            ->addHeader('Content-Type', 'application/json')
631
            ->setBody(Convert::raw2json($data));
632
    }
633
634
    /**
635
     * Update the position and parent of a tree node.
636
     * Only saves the node if changes were made.
637
     *
638
     * Required data:
639
     * - 'ID': The moved node
640
     * - 'ParentID': New parent relation of the moved node (0 for root)
641
     * - 'SiblingIDs': Array of all sibling nodes to the moved node (incl. the node itself).
642
     *   In case of a 'ParentID' change, relates to the new siblings under the new parent.
643
     *
644
     * @param HTTPRequest $request
645
     * @return HTTPResponse JSON string with a
646
     * @throws HTTPResponse_Exception
647
     */
648
    public function savetreenode($request)
649
    {
650
        if (!SecurityToken::inst()->checkRequest($request)) {
651
            return $this->httpError(400);
652
        }
653
        if (!Permission::check('SITETREE_REORGANISE') && !Permission::check('ADMIN')) {
654
            return $this->httpError(
655
                403,
656
                _t(
657
                    'LeftAndMain.CANT_REORGANISE',
658
                    "You do not have permission to rearange the site tree. Your change was not saved."
659
                )
660
            );
661
        }
662
663
        $className = $this->stat('tree_class');
664
        $id = $request->requestVar('ID');
665
        $parentID = $request->requestVar('ParentID');
666
        if (!is_numeric($id) || !is_numeric($parentID)) {
667
            return $this->httpError(400);
668
        }
669
670
        // Check record exists in the DB
671
        /** @var SiteTree $node */
672
        $node = DataObject::get_by_id($className, $id);
673
        if (!$node) {
674
            return $this->httpError(
675
                500,
676
                _t(
677
                    'LeftAndMain.PLEASESAVE',
678
                    "Please Save Page: This page could not be updated because it hasn't been saved yet."
679
                )
680
            );
681
        }
682
683
        // Check top level permissions
684
        $root = $node->getParentType();
685
        if (($parentID == '0' || $root == 'root') && !SiteConfig::current_site_config()->canCreateTopLevel()) {
686
            return $this->httpError(
687
                403,
688
                _t(
689
                    'LeftAndMain.CANT_REORGANISE',
690
                    "You do not have permission to alter Top level pages. Your change was not saved."
691
                )
692
            );
693
        }
694
695
        $siblingIDs = $request->requestVar('SiblingIDs');
696
        $statusUpdates = array('modified'=>array());
697
698
        if (!$node->canEdit()) {
699
            return Security::permissionFailure($this);
700
        }
701
702
        // Update hierarchy (only if ParentID changed)
703
        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...
704
            $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...
705
            $node->write();
706
707
            $statusUpdates['modified'][$node->ID] = array(
708
                '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...
709
            );
710
711
            // Update all dependent pages
712
            $virtualPages = VirtualPage::get()->filter("CopyContentFromID", $node->ID);
713
            foreach ($virtualPages as $virtualPage) {
714
                $statusUpdates['modified'][$virtualPage->ID] = array(
715
                    'TreeTitle' => $virtualPage->TreeTitle()
716
                );
717
            }
718
719
            $this->getResponse()->addHeader(
720
                'X-Status',
721
                rawurlencode(_t('LeftAndMain.REORGANISATIONSUCCESSFUL', 'Reorganised the site tree successfully.'))
722
            );
723
        }
724
725
        // Update sorting
726
        if (is_array($siblingIDs)) {
727
            $counter = 0;
728
            foreach ($siblingIDs as $id) {
729
                if ($id == $node->ID) {
730
                    $node->Sort = ++$counter;
731
                    $node->write();
732
                    $statusUpdates['modified'][$node->ID] = array(
733
                        '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...
734
                    );
735
                } elseif (is_numeric($id)) {
736
                    // Nodes that weren't "actually moved" shouldn't be registered as
737
                    // having been edited; do a direct SQL update instead
738
                    ++$counter;
739
                    $table = DataObject::getSchema()->baseDataTable($className);
740
                    DB::prepared_query(
741
                        "UPDATE \"$table\" SET \"Sort\" = ? WHERE \"ID\" = ?",
742
                        array($counter, $id)
743
                    );
744
                }
745
            }
746
747
            $this->getResponse()->addHeader(
748
                'X-Status',
749
                rawurlencode(_t('LeftAndMain.REORGANISATIONSUCCESSFUL', 'Reorganised the site tree successfully.'))
750
            );
751
        }
752
753
        return $this
754
            ->getResponse()
755
            ->addHeader('Content-Type', 'application/json')
756
            ->setBody(Convert::raw2json($statusUpdates));
757
    }
758
759
    public function CanOrganiseSitetree()
760
    {
761
        return !Permission::check('SITETREE_REORGANISE') && !Permission::check('ADMIN') ? false : true;
762
    }
763
764
    /**
765
     * @return boolean
766
     */
767
    public function TreeIsFiltered()
768
    {
769
        $query = $this->getRequest()->getVar('q');
770
771
        if (!$query || (count($query) === 1 && isset($query['FilterClass']) && $query['FilterClass'] === 'SilverStripe\\CMS\\Controllers\\CMSSiteTreeFilter_Search')) {
772
            return false;
773
        }
774
775
        return true;
776
    }
777
778
    public function ExtraTreeTools()
779
    {
780
        $html = '';
781
        $this->extend('updateExtraTreeTools', $html);
782
        return $html;
783
    }
784
785
    /**
786
     * Returns a Form for page searching for use in templates.
787
     *
788
     * Can be modified from a decorator by a 'updateSearchForm' method
789
     *
790
     * @return Form
791
     */
792
    public function SearchForm()
793
    {
794
        // Create the fields
795
        $content = new TextField('q[Term]', _t('CMSSearch.FILTERLABELTEXT', 'Search'));
796
        $dateFrom = new DateField(
797
            'q[LastEditedFrom]',
798
            _t('CMSSearch.FILTERDATEFROM', 'From')
799
        );
800
        $dateTo = new DateField(
801
            'q[LastEditedTo]',
802
            _t('CMSSearch.FILTERDATETO', 'To')
803
        );
804
        $pageFilter = new DropdownField(
805
            'q[FilterClass]',
806
            _t('CMSMain.PAGES', 'Page status'),
807
            CMSSiteTreeFilter::get_all_filters()
808
        );
809
        $pageClasses = new DropdownField(
810
            'q[ClassName]',
811
            _t('CMSMain.PAGETYPEOPT', 'Page type', 'Dropdown for limiting search to a page type'),
812
            $this->getPageTypes()
813
        );
814
        $pageClasses->setEmptyString(_t('CMSMain.PAGETYPEANYOPT', 'Any'));
815
816
        // Group the Datefields
817
        $dateGroup = new FieldGroup(
818
            $dateFrom,
819
            $dateTo
820
        );
821
        $dateGroup->setTitle(_t('CMSSearch.PAGEFILTERDATEHEADING', 'Last edited'));
822
823
        // view mode
824
        $viewMode = HiddenField::create('view', false, $this->ViewState());
825
826
        // Create the Field list
827
        $fields = new FieldList(
828
            $content,
829
            $pageFilter,
830
            $pageClasses,
831
            $dateGroup,
832
            $viewMode
833
        );
834
835
        // Create the Search and Reset action
836
        $actions = new FieldList(
837
            FormAction::create('doSearch', _t('CMSMain_left_ss.APPLY_FILTER', 'Search'))
838
                ->addExtraClass('btn btn-primary'),
839
            ResetFormAction::create('clear', _t('CMSMain_left_ss.CLEAR_FILTER', 'Clear'))
840
                ->addExtraClass('btn btn-secondary')
841
        );
842
843
        // Use <button> to allow full jQuery UI styling on the all of the Actions
844
        /** @var FormAction $action */
845
        foreach ($actions->dataFields() as $action) {
846
            /** @var FormAction $action */
847
            $action->setUseButtonTag(true);
848
        }
849
850
        // Create the form
851
        /** @skipUpgrade */
852
        $form = Form::create($this, 'SearchForm', $fields, $actions)
853
            ->addExtraClass('cms-search-form')
854
            ->setFormMethod('GET')
855
            ->setFormAction($this->Link())
856
            ->disableSecurityToken()
857
            ->unsetValidator();
858
859
        // Load the form with previously sent search data
860
        $form->loadDataFrom($this->getRequest()->getVars());
861
862
        // Allow decorators to modify the form
863
        $this->extend('updateSearchForm', $form);
864
865
        return $form;
866
    }
867
868
    /**
869
     * Returns a sorted array suitable for a dropdown with pagetypes and their translated name
870
     *
871
     * @return array
872
     */
873
    protected function getPageTypes()
874
    {
875
        $pageTypes = array();
876
        foreach (SiteTree::page_type_classes() as $pageTypeClass) {
877
            $pageTypes[$pageTypeClass] = SiteTree::singleton($pageTypeClass)->i18n_singular_name();
878
        }
879
        asort($pageTypes);
880
        return $pageTypes;
881
    }
882
883
    public function doSearch($data, $form)
0 ignored issues
show
Unused Code introduced by
The parameter $data is not used and could be removed.

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

Loading history...
Unused Code introduced by
The parameter $form is not used and could be removed.

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

Loading history...
884
    {
885
        return $this->getsubtree($this->getRequest());
886
    }
887
888
    /**
889
     * @param bool $unlinked
890
     * @return ArrayList
891
     */
892
    public function Breadcrumbs($unlinked = false)
893
    {
894
        $items = parent::Breadcrumbs($unlinked);
895
896
        if ($items->count() > 1) {
897
            // Specific to the SiteTree admin section, we never show the cms section and current
898
            // page in the same breadcrumbs block.
899
            $items->shift();
900
        }
901
902
        return $items;
903
    }
904
905
    /**
906
     * Create serialized JSON string with site tree hints data to be injected into
907
     * 'data-hints' attribute of root node of jsTree.
908
     *
909
     * @return string Serialized JSON
910
     */
911
    public function SiteTreeHints()
912
    {
913
        $classes = SiteTree::page_type_classes();
914
915
        $cacheCanCreate = array();
916
        foreach ($classes as $class) {
917
            $cacheCanCreate[$class] = singleton($class)->canCreate();
918
        }
919
920
        // Generate basic cache key. Too complex to encompass all variations
921
        $cache = Injector::inst()->get(CacheInterface::class . '.CMSMain_SiteTreeHints');
922
        $cacheKey = md5(implode('_', array(Member::currentUserID(), implode(',', $cacheCanCreate), implode(',', $classes))));
923
        if ($this->getRequest()->getVar('flush')) {
924
            $cache->clear();
925
        }
926
        $json = $cache->get($cacheKey);
927
        if (!$json) {
928
            $def['Root'] = array();
0 ignored issues
show
Coding Style Comprehensibility introduced by
$def was never initialized. Although not strictly required by PHP, it is generally a good practice to add $def = array(); before regardless.

Adding an explicit array definition is generally preferable to implicit array definition as it guarantees a stable state of the code.

Let’s take a look at an example:

foreach ($collection as $item) {
    $myArray['foo'] = $item->getFoo();

    if ($item->hasBar()) {
        $myArray['bar'] = $item->getBar();
    }

    // do something with $myArray
}

As you can see in this example, the array $myArray is initialized the first time when the foreach loop is entered. You can also see that the value of the bar key is only written conditionally; thus, its value might result from a previous iteration.

This might or might not be intended. To make your intention clear, your code more readible and to avoid accidental bugs, we recommend to add an explicit initialization $myArray = array() either outside or inside the foreach loop.

Loading history...
929
            $def['Root']['disallowedChildren'] = array();
930
931
            // Contains all possible classes to support UI controls listing them all,
932
            // such as the "add page here" context menu.
933
            $def['All'] = array();
934
935
            // Identify disallows and set globals
936
            foreach ($classes as $class) {
937
                $obj = singleton($class);
938
                if ($obj instanceof HiddenClass) {
939
                    continue;
940
                }
941
942
                // Name item
943
                $def['All'][$class] = array(
944
                    'title' => $obj->i18n_singular_name()
945
                );
946
947
                // Check if can be created at the root
948
                $needsPerm = $obj->stat('need_permission');
949
                if (!$obj->stat('can_be_root')
950
                    || (!array_key_exists($class, $cacheCanCreate) || !$cacheCanCreate[$class])
951
                    || ($needsPerm && !$this->can($needsPerm))
952
                ) {
953
                    $def['Root']['disallowedChildren'][] = $class;
954
                }
955
956
                // Hint data specific to the class
957
                $def[$class] = array();
958
959
                $defaultChild = $obj->defaultChild();
960
                if ($defaultChild !== 'Page' && $defaultChild !== null) {
961
                    $def[$class]['defaultChild'] = $defaultChild;
962
                }
963
964
                $defaultParent = $obj->defaultParent();
965
                if ($defaultParent !== 1 && $defaultParent !== null) {
966
                    $def[$class]['defaultParent'] = $defaultParent;
967
                }
968
            }
969
970
            $this->extend('updateSiteTreeHints', $def);
971
972
            $json = Convert::raw2json($def);
973
            $cache->set($cacheKey, $json);
974
        }
975
        return $json;
976
    }
977
978
    /**
979
     * Populates an array of classes in the CMS
980
     * which allows the user to change the page type.
981
     *
982
     * @return SS_List
983
     */
984
    public function PageTypes()
985
    {
986
        $classes = SiteTree::page_type_classes();
987
988
        $result = new ArrayList();
989
990
        foreach ($classes as $class) {
991
            $instance = SiteTree::singleton($class);
992
            if ($instance instanceof HiddenClass) {
993
                continue;
994
            }
995
996
            // skip this type if it is restricted
997
            if ($instance->stat('need_permission') && !$this->can(singleton($class)->stat('need_permission'))) {
998
                continue;
999
            }
1000
1001
            $singularName = $instance->i18n_singular_name();
1002
            $description = $instance->i18n_classDescription();
1003
1004
            $result->push(new ArrayData(array(
1005
                'ClassName' => $class,
1006
                'AddAction' => $singularName,
1007
                'Description' => $description,
1008
                // TODO Sprite support
1009
                'IconURL' => $instance->stat('icon'),
1010
                'Title' => $singularName,
1011
            )));
1012
        }
1013
1014
        $result = $result->sort('AddAction');
1015
1016
        return $result;
1017
    }
1018
1019
    /**
1020
     * Get a database record to be managed by the CMS.
1021
     *
1022
     * @param int $id Record ID
1023
     * @param int $versionID optional Version id of the given record
1024
     * @return SiteTree
1025
     */
1026
    public function getRecord($id, $versionID = null)
1027
    {
1028
        if (!$id) {
1029
            return null;
1030
        }
1031
        $treeClass = $this->stat('tree_class');
1032
        if ($id instanceof $treeClass) {
1033
            return $id;
1034
        }
1035
        if (substr($id, 0, 3) == 'new') {
1036
            return $this->getNewItem($id);
1037
        }
1038
        if (!is_numeric($id)) {
1039
            return null;
1040
        }
1041
1042
        $currentStage = Versioned::get_reading_mode();
1043
1044
        if ($this->getRequest()->getVar('Version')) {
1045
            $versionID = (int) $this->getRequest()->getVar('Version');
1046
        }
1047
1048
        /** @var SiteTree $record */
1049
        if ($versionID) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $versionID of type integer|null is loosely compared to true; this is ambiguous if the integer can be zero. You might want to explicitly use !== null instead.

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

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
1050
            $record = Versioned::get_version($treeClass, $id, $versionID);
1051
        } else {
1052
            $record = DataObject::get_by_id($treeClass, $id);
1053
        }
1054
1055
        // Then, try getting a record from the live site
1056
        if (!$record) {
1057
            // $record = Versioned::get_one_by_stage($treeClass, "Live", "\"$treeClass\".\"ID\" = $id");
1058
            Versioned::set_stage(Versioned::LIVE);
1059
            singleton($treeClass)->flushCache();
1060
1061
            $record = DataObject::get_by_id($treeClass, $id);
1062
        }
1063
1064
        // Then, try getting a deleted record
1065
        if (!$record) {
1066
            $record = Versioned::get_latest_version($treeClass, $id);
1067
        }
1068
1069
        // Set the reading mode back to what it was.
1070
        Versioned::set_reading_mode($currentStage);
1071
1072
        return $record;
1073
    }
1074
1075
    /**
1076
     * {@inheritdoc}
1077
     *
1078
     * @param HTTPRequest $request
1079
     * @return Form
1080
     */
1081
    public function EditForm($request = null)
1082
    {
1083
        // set page ID from request
1084
        if ($request) {
1085
            // Validate id is present
1086
            $id = $request->param('ID');
1087
            if (!isset($id)) {
1088
                $this->httpError(400);
1089
                return null;
1090
            }
1091
            $this->setCurrentPageID($id);
1092
        }
1093
        return $this->getEditForm();
1094
    }
1095
1096
    /**
1097
     * @param int $id
1098
     * @param FieldList $fields
1099
     * @return Form
1100
     */
1101
    public function getEditForm($id = null, $fields = null)
1102
    {
1103
        // Get record
1104
        if (!$id) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $id of type integer|null is loosely compared to false; this is ambiguous if the integer can be zero. You might want to explicitly use === null instead.

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

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
1105
            $id = $this->currentPageID();
1106
        }
1107
        /** @var SiteTree $record */
1108
        $record = $this->getRecord($id);
1109
1110
        // Check parent form can be generated
1111
        $form = parent::getEditForm($record, $fields);
0 ignored issues
show
Documentation introduced by
$record is of type object<SilverStripe\CMS\Model\SiteTree>, but the function expects a integer|null.

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1112
        if (!$form || !$record) {
1113
            return $form;
1114
        }
1115
1116
        if (!$fields) {
1117
            $fields = $form->Fields();
1118
        }
1119
1120
        // Add extra fields
1121
        $deletedFromStage = !$record->isOnDraft();
0 ignored issues
show
Documentation Bug introduced by
The method isOnDraft does not exist on object<SilverStripe\CMS\Model\SiteTree>? Since you implemented __call, maybe consider adding a @method annotation.

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

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

class ParentClass {
    private $data = array();

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

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

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
1122
        $fields->push($idField = new HiddenField("ID", false, $id));
0 ignored issues
show
Documentation introduced by
false is of type boolean, but the function expects a null|string.

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1123
        // Necessary for different subsites
1124
        $fields->push($liveLinkField = new HiddenField("AbsoluteLink", false, $record->AbsoluteLink()));
0 ignored issues
show
Documentation introduced by
false is of type boolean, but the function expects a null|string.

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1125
        $fields->push($liveLinkField = new HiddenField("LiveLink"));
1126
        $fields->push($stageLinkField = new HiddenField("StageLink"));
1127
        $fields->push($archiveWarningMsgField = new HiddenField("ArchiveWarningMessage"));
1128
        $fields->push(new HiddenField("TreeTitle", false, $record->getTreeTitle()));
0 ignored issues
show
Documentation introduced by
false is of type boolean, but the function expects a null|string.

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1129
1130
        $archiveWarningMsgField->setValue($this->getArchiveWarningMessage($record));
1131
1132
        // Build preview / live links
1133
        $liveLink = $record->getAbsoluteLiveLink();
1134
        if ($liveLink) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $liveLink of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

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

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

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

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
1135
            $liveLinkField->setValue($liveLink);
1136
        }
1137
        if (!$deletedFromStage) {
1138
            $stageLink = Controller::join_links($record->AbsoluteLink(), '?stage=Stage');
1139
            if ($stageLink) {
1140
                $stageLinkField->setValue($stageLink);
1141
            }
1142
        }
1143
1144
        // Added in-line to the form, but plucked into different view by LeftAndMain.Preview.js upon load
1145
        /** @skipUpgrade */
1146
        if ($record instanceof CMSPreviewable && !$fields->fieldByName('SilverStripeNavigator')) {
1147
            $navField = new LiteralField('SilverStripeNavigator', $this->getSilverStripeNavigator());
1148
            $navField->setAllowHTML(true);
1149
            $fields->push($navField);
1150
        }
1151
1152
        // getAllCMSActions can be used to completely redefine the action list
1153
        if ($record->hasMethod('getAllCMSActions')) {
1154
            $actions = $record->getAllCMSActions();
0 ignored issues
show
Documentation Bug introduced by
The method getAllCMSActions does not exist on object<SilverStripe\CMS\Model\SiteTree>? Since you implemented __call, maybe consider adding a @method annotation.

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

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

class ParentClass {
    private $data = array();

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

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

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
1155
        } else {
1156
            $actions = $record->getCMSActions();
1157
1158
            // Find and remove action menus that have no actions.
1159
            if ($actions && $actions->count()) {
1160
                /** @var TabSet $tabset */
1161
                $tabset = $actions->fieldByName('ActionMenus');
1162
                if ($tabset) {
1163
                    /** @var Tab $tab */
1164
                    foreach ($tabset->getChildren() as $tab) {
1165
                        if (!$tab->getChildren()->count()) {
1166
                            $tabset->removeByName($tab->getName());
1167
                        }
1168
                    }
1169
                }
1170
            }
1171
        }
1172
1173
        // Use <button> to allow full jQuery UI styling
1174
        $actionsFlattened = $actions->dataFields();
1175
        if ($actionsFlattened) {
1176
            /** @var FormAction $action */
1177
            foreach ($actionsFlattened as $action) {
1178
                $action->setUseButtonTag(true);
1179
            }
1180
        }
1181
1182
        // TODO Can't merge $FormAttributes in template at the moment
1183
        $form->addExtraClass('center ' . $this->BaseCSSClasses());
1184
        // Set validation exemptions for specific actions
1185
        $form->setValidationExemptActions(array('restore', 'revert', 'deletefromlive', 'delete', 'unpublish', 'rollback', 'doRollback'));
1186
1187
        // Announce the capability so the frontend can decide whether to allow preview or not.
1188
        if ($record instanceof CMSPreviewable) {
1189
            $form->addExtraClass('cms-previewable');
1190
        }
1191
        $form->addExtraClass('fill-height flexbox-area-grow');
1192
1193
        if (!$record->canEdit() || $deletedFromStage) {
1194
            $readonlyFields = $form->Fields()->makeReadonly();
1195
            $form->setFields($readonlyFields);
1196
        }
1197
1198
        $form->Fields()->setForm($form);
1199
1200
        $this->extend('updateEditForm', $form);
1201
1202
        // Use custom reqest handler for LeftAndMain requests;
1203
        // CMS Forms cannot be identified solely by name, but also need ID (and sometimes OtherID)
1204
        $form->setRequestHandler(
1205
            LeftAndMainFormRequestHandler::create($form, [$id])
1206
        );
1207
        return $form;
1208
    }
1209
1210
    public function EmptyForm()
1211
    {
1212
        $fields = new FieldList(
1213
            new LabelField('PageDoesntExistLabel', _t('CMSMain.PAGENOTEXISTS', "This page doesn't exist"))
1214
        );
1215
        $form = parent::EmptyForm();
1216
        $form->setFields($fields);
1217
        $fields->setForm($form);
1218
        return $form;
1219
    }
1220
1221
    /**
1222
     * Build an archive warning message based on the page's children
1223
     *
1224
     * @param SiteTree $record
1225
     * @return string
1226
     */
1227
    protected function getArchiveWarningMessage($record)
1228
    {
1229
        // Get all page's descendants
1230
        $record->collateDescendants(true, $descendants);
0 ignored issues
show
Documentation introduced by
true is of type boolean, but the function expects a string.

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

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

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

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

Loading history...
1232
            $descendants = [];
1233
        }
1234
1235
        // Get all campaigns that the page and its descendants belong to
1236
        $inChangeSetIDs = ChangeSetItem::get_for_object($record)->column('ChangeSetID');
1237
1238
        foreach ($descendants as $page) {
1239
            $inChangeSetIDs = array_merge($inChangeSetIDs, ChangeSetItem::get_for_object($page)->column('ChangeSetID'));
1240
        }
1241
1242
        if (count($inChangeSetIDs) > 0) {
1243
            $inChangeSets = ChangeSet::get()->filter(['ID' => $inChangeSetIDs, 'State' => ChangeSet::STATE_OPEN]);
1244
        } else {
1245
            $inChangeSets = new ArrayList();
1246
        }
1247
1248
        $numCampaigns = ChangeSet::singleton()->i18n_pluralise($inChangeSets->count());
1249
        $numCampaigns = mb_strtolower($numCampaigns);
1250
1251
        if (count($descendants) > 0 && $inChangeSets->count() > 0) {
1252
            $archiveWarningMsg = _t('CMSMain.ArchiveWarningWithChildrenAndCampaigns', 'Warning: This page and all of its child pages will be unpublished and automatically removed from their associated {NumCampaigns} before being sent to the archive.\n\nAre you sure you want to proceed?', [ 'NumCampaigns' => $numCampaigns ]);
1253
        } elseif (count($descendants) > 0) {
1254
            $archiveWarningMsg = _t('CMSMain.ArchiveWarningWithChildren', 'Warning: This page and all of its child pages will be unpublished before being sent to the archive.\n\nAre you sure you want to proceed?');
1255
        } elseif ($inChangeSets->count() > 0) {
1256
            $archiveWarningMsg = _t('CMSMain.ArchiveWarningWithCampaigns', 'Warning: This page will be unpublished and automatically removed from their associated {NumCampaigns} before being sent to the archive.\n\nAre you sure you want to proceed?', [ 'NumCampaigns' => $numCampaigns ]);
1257
        } else {
1258
            $archiveWarningMsg = _t('CMSMain.ArchiveWarning', 'Warning: This page will be unpublished before being sent to the archive.\n\nAre you sure you want to proceed?');
1259
        }
1260
1261
        return $archiveWarningMsg;
1262
    }
1263
1264
    /**
1265
     * @param HTTPRequest $request
1266
     * @return string HTML
1267
     */
1268
    public function treeview($request)
1269
    {
1270
        return $this->getResponseNegotiator()->respond($request);
1271
    }
1272
1273
    /**
1274
     * @param HTTPRequest $request
1275
     * @return string HTML
1276
     */
1277
    public function listview($request)
1278
    {
1279
        return $this->getResponseNegotiator()->respond($request);
1280
    }
1281
1282
    /**
1283
     * @return string
1284
     */
1285
    public function ViewState()
1286
    {
1287
        $mode = $this->getRequest()->requestVar('view')
1288
            ?: $this->getRequest()->param('Action');
1289
        switch ($mode) {
1290
            case 'listview':
1291
            case 'treeview':
1292
                return $mode;
1293
            default:
1294
                return 'treeview';
1295
        }
1296
    }
1297
1298
    /**
1299
     * Callback to request the list of page types allowed under a given page instance.
1300
     * Provides a slower but more precise response over SiteTreeHints
1301
     *
1302
     * @param HTTPRequest $request
1303
     * @return HTTPResponse
1304
     */
1305
    public function childfilter($request)
1306
    {
1307
        // Check valid parent specified
1308
        $parentID = $request->requestVar('ParentID');
1309
        $parent = SiteTree::get()->byID($parentID);
1310
        if (!$parent || !$parent->exists()) {
1311
            return $this->httpError(404);
1312
        }
1313
1314
        // Build hints specific to this class
1315
        // Identify disallows and set globals
1316
        $classes = SiteTree::page_type_classes();
1317
        $disallowedChildren = array();
1318
        foreach ($classes as $class) {
1319
            $obj = singleton($class);
1320
            if ($obj instanceof HiddenClass) {
1321
                continue;
1322
            }
1323
1324
            if (!$obj->canCreate(null, array('Parent' => $parent))) {
1325
                $disallowedChildren[] = $class;
1326
            }
1327
        }
1328
1329
        $this->extend('updateChildFilter', $disallowedChildren, $parentID);
1330
        return $this
1331
            ->getResponse()
1332
            ->addHeader('Content-Type', 'application/json; charset=utf-8')
1333
            ->setBody(Convert::raw2json($disallowedChildren));
1334
    }
1335
1336
    /**
1337
     * Safely reconstruct a selected filter from a given set of query parameters
1338
     *
1339
     * @param array $params Query parameters to use
1340
     * @return CMSSiteTreeFilter The filter class, or null if none present
1341
     * @throws InvalidArgumentException if invalid filter class is passed.
1342
     */
1343
    protected function getQueryFilter($params)
1344
    {
1345
        if (empty($params['FilterClass'])) {
1346
            return null;
1347
        }
1348
        $filterClass = $params['FilterClass'];
1349
        if (!is_subclass_of($filterClass, CMSSiteTreeFilter::class)) {
0 ignored issues
show
Bug introduced by
Due to PHP Bug #53727, is_subclass_of might return inconsistent results on some PHP versions if \SilverStripe\CMS\Contro...MSSiteTreeFilter::class can be an interface. If so, you could instead use ReflectionClass::implementsInterface.
Loading history...
1350
            throw new InvalidArgumentException("Invalid filter class passed: {$filterClass}");
1351
        }
1352
        return $filterClass::create($params);
1353
    }
1354
1355
    /**
1356
     * Returns the pages meet a certain criteria as {@see CMSSiteTreeFilter} or the subpages of a parent page
1357
     * defaulting to no filter and show all pages in first level.
1358
     * Doubles as search results, if any search parameters are set through {@link SearchForm()}.
1359
     *
1360
     * @param array $params Search filter criteria
1361
     * @param int $parentID Optional parent node to filter on (can't be combined with other search criteria)
1362
     * @return SS_List
1363
     * @throws InvalidArgumentException if invalid filter class is passed.
1364
     */
1365
    public function getList($params = array(), $parentID = 0)
1366
    {
1367
        if ($filter = $this->getQueryFilter($params)) {
1368
            return $filter->getFilteredPages();
1369
        } else {
1370
            $list = DataList::create($this->stat('tree_class'));
1371
            $parentID = is_numeric($parentID) ? $parentID : 0;
1372
            return $list->filter("ParentID", $parentID);
1373
        }
1374
    }
1375
1376
    /**
1377
     * @return Form
1378
     */
1379
    public function ListViewForm()
1380
    {
1381
        $params = $this->getRequest()->requestVar('q');
1382
        $list = $this->getList($params, $parentID = $this->getRequest()->requestVar('ParentID'));
1383
        $gridFieldConfig = GridFieldConfig::create()->addComponents(
1384
            new GridFieldSortableHeader(),
1385
            new GridFieldDataColumns(),
1386
            new GridFieldPaginator($this->config()->get('page_length'))
1387
        );
1388
        if ($parentID) {
1389
            $linkSpec = $this->Link();
1390
            $linkSpec = $linkSpec . (strstr($linkSpec, '?') ? '&' : '?') . 'ParentID=%d&view=listview';
1391
            $gridFieldConfig->addComponent(
1392
                GridFieldLevelup::create($parentID)
1393
                    ->setLinkSpec($linkSpec)
1394
                    ->setAttributes(array('data-pjax' => 'ListViewForm,Breadcrumbs'))
1395
            );
1396
        }
1397
        $gridField = new GridField('Page', 'Pages', $list, $gridFieldConfig);
1398
        /** @var GridFieldDataColumns $columns */
1399
        $columns = $gridField->getConfig()->getComponentByType('SilverStripe\\Forms\\GridField\\GridFieldDataColumns');
1400
1401
        // Don't allow navigating into children nodes on filtered lists
1402
        $fields = array(
1403
            'getTreeTitle' => _t('SiteTree.PAGETITLE', 'Page Title'),
1404
            'singular_name' => _t('SiteTree.PAGETYPE', 'Page Type'),
1405
            'LastEdited' => _t('SiteTree.LASTUPDATED', 'Last Updated'),
1406
        );
1407
        /** @var GridFieldSortableHeader $sortableHeader */
1408
        $sortableHeader = $gridField->getConfig()->getComponentByType('SilverStripe\\Forms\\GridField\\GridFieldSortableHeader');
1409
        $sortableHeader->setFieldSorting(array('getTreeTitle' => 'Title'));
1410
        $gridField->getState()->ParentID = $parentID;
1411
1412
        if (!$params) {
1413
            $fields = array_merge(array('listChildrenLink' => ''), $fields);
1414
        }
1415
1416
        $columns->setDisplayFields($fields);
1417
        $columns->setFieldCasting(array(
1418
            'Created' => 'DBDatetime->Ago',
1419
            'LastEdited' => 'DBDatetime->FormatFromSettings',
1420
            'getTreeTitle' => 'HTMLFragment'
1421
        ));
1422
1423
        $controller = $this;
1424
        $columns->setFieldFormatting(array(
1425
            'listChildrenLink' => function ($value, &$item) use ($controller) {
1426
                /** @var SiteTree $item */
1427
                $num = $item ? $item->numChildren() : null;
0 ignored issues
show
Documentation Bug introduced by
The method numChildren does not exist on object<SilverStripe\CMS\Model\SiteTree>? Since you implemented __call, maybe consider adding a @method annotation.

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

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

class ParentClass {
    private $data = array();

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

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

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
1428
                if ($num) {
1429
                    return sprintf(
1430
                        '<a class="btn btn-secondary btn--no-text btn--icon-large font-icon-right-dir cms-panel-link list-children-link" data-pjax-target="ListViewForm,Breadcrumbs" href="%s"><span class="sr-only">%s child pages</span></a>',
1431
                        Controller::join_links(
1432
                            $controller->Link(),
1433
                            sprintf("?ParentID=%d&view=listview", (int)$item->ID)
1434
                        ),
1435
                        $num
1436
                    );
1437
                }
1438
            },
1439
            'getTreeTitle' => function ($value, &$item) use ($controller) {
1440
                return sprintf(
1441
                    '<a class="action-detail" href="%s">%s</a>',
1442
                    Controller::join_links(
1443
                        CMSPageEditController::singleton()->Link('show'),
1444
                        (int)$item->ID
1445
                    ),
1446
                    $item->TreeTitle // returns HTML, does its own escaping
1447
                );
1448
            }
1449
        ));
1450
1451
        $negotiator = $this->getResponseNegotiator();
1452
        $listview = Form::create(
1453
            $this,
1454
            'ListViewForm',
1455
            new FieldList($gridField),
1456
            new FieldList()
1457
        )->setHTMLID('Form_ListViewForm');
1458
        $listview->setAttribute('data-pjax-fragment', 'ListViewForm');
1459 View Code Duplication
        $listview->setValidationResponseCallback(function (ValidationResult $errors) use ($negotiator, $listview) {
0 ignored issues
show
Unused Code introduced by
The parameter $errors is not used and could be removed.

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

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

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

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

Loading history...
1460
            $request = $this->getRequest();
1461
            if ($request->isAjax() && $negotiator) {
1462
                $result = $listview->forTemplate();
1463
                return $negotiator->respond($request, array(
1464
                    'CurrentForm' => function () use ($result) {
1465
                        return $result;
1466
                    }
1467
                ));
1468
            }
1469
        });
1470
1471
        $this->extend('updateListView', $listview);
1472
1473
        $listview->disableSecurityToken();
1474
        return $listview;
1475
    }
1476
1477
    public function currentPageID()
1478
    {
1479
        $id = parent::currentPageID();
1480
1481
        $this->extend('updateCurrentPageID', $id);
1482
1483
        return $id;
1484
    }
1485
1486
    //------------------------------------------------------------------------------------------//
1487
    // Data saving handlers
1488
1489
    /**
1490
     * Save and Publish page handler
1491
     *
1492
     * @param array $data
1493
     * @param Form $form
1494
     * @return HTTPResponse
1495
     * @throws HTTPResponse_Exception
1496
     */
1497
    public function save($data, $form)
1498
    {
1499
        $className = $this->stat('tree_class');
1500
1501
        // Existing or new record?
1502
        $id = $data['ID'];
1503
        if (substr($id, 0, 3) != 'new') {
1504
            /** @var SiteTree $record */
1505
            $record = DataObject::get_by_id($className, $id);
1506
            // Check edit permissions
1507
            if ($record && !$record->canEdit()) {
1508
                return Security::permissionFailure($this);
1509
            }
1510
            if (!$record || !$record->ID) {
1511
                throw new HTTPResponse_Exception("Bad record ID #$id", 404);
1512
            }
1513
        } else {
1514
            if (!$className::singleton()->canCreate()) {
1515
                return Security::permissionFailure($this);
1516
            }
1517
            $record = $this->getNewItem($id, false);
1518
        }
1519
1520
        // Check publishing permissions
1521
        $doPublish = !empty($data['publish']);
1522
        if ($record && $doPublish && !$record->canPublish()) {
1523
            return Security::permissionFailure($this);
1524
        }
1525
1526
        // TODO Coupling to SiteTree
1527
        $record->HasBrokenLink = 0;
1528
        $record->HasBrokenFile = 0;
1529
1530
        if (!$record->ObsoleteClassName) {
1531
            $record->writeWithoutVersion();
1532
        }
1533
1534
        // Update the class instance if necessary
1535
        if (isset($data['ClassName']) && $data['ClassName'] != $record->ClassName) {
1536
            // Replace $record with a new instance of the new class
1537
            $newClassName = $data['ClassName'];
1538
            $record = $record->newClassInstance($newClassName);
1539
        }
1540
1541
        // save form data into record
1542
        $form->saveInto($record);
1543
        $record->write();
1544
1545
        // If the 'Save & Publish' button was clicked, also publish the page
1546
        if ($doPublish) {
1547
            $record->publishRecursive();
1548
            $message = _t(
1549
                'CMSMain.PUBLISHED',
1550
                "Published '{title}' successfully.",
1551
                ['title' => $record->Title]
1552
            );
1553
        } else {
1554
            $message = _t(
1555
                'CMSMain.SAVED',
1556
                "Saved '{title}' successfully.",
1557
                ['title' => $record->Title]
1558
            );
1559
        }
1560
1561
        $this->getResponse()->addHeader('X-Status', rawurlencode($message));
1562
        return $this->getResponseNegotiator()->respond($this->getRequest());
1563
    }
1564
1565
    /**
1566
     * @uses LeftAndMainExtension->augmentNewSiteTreeItem()
1567
     *
1568
     * @param int|string $id
1569
     * @param bool $setID
1570
     * @return mixed|DataObject
1571
     * @throws HTTPResponse_Exception
1572
     */
1573
    public function getNewItem($id, $setID = true)
1574
    {
1575
        $parentClass = $this->stat('tree_class');
1576
        list($dummy, $className, $parentID, $suffix) = array_pad(explode('-', $id), 4, null);
0 ignored issues
show
Unused Code introduced by
The assignment to $dummy is unused. Consider omitting it like so list($first,,$third).

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

Consider the following code example.

<?php

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

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

print $a . " - " . $c;

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

Instead, the list call could have been.

list($a,, $c) = returnThreeValues();
Loading history...
1577
1578
        if (!is_a($className, $parentClass, true)) {
1579
            $response = Security::permissionFailure($this);
1580
            if (!$response) {
1581
                $response = $this->getResponse();
1582
            }
1583
            throw new HTTPResponse_Exception($response);
1584
        }
1585
1586
        /** @var SiteTree $newItem */
1587
        $newItem = Injector::inst()->create($className);
1588
        if (!$suffix) {
1589
            $sessionTag = "NewItems." . $parentID . "." . $className;
1590
            if (Session::get($sessionTag)) {
1591
                $suffix = '-' . Session::get($sessionTag);
1592
                Session::set($sessionTag, Session::get($sessionTag) + 1);
1593
            } else {
1594
                Session::set($sessionTag, 1);
1595
            }
1596
1597
                $id = $id . $suffix;
1598
        }
1599
1600
        $newItem->Title = _t(
1601
            'CMSMain.NEWPAGE',
1602
            "New {pagetype}",
1603
            'followed by a page type title',
1604
            array('pagetype' => singleton($className)->i18n_singular_name())
1605
        );
1606
        $newItem->ClassName = $className;
1607
        $newItem->ParentID = $parentID;
0 ignored issues
show
Documentation introduced by
The property ParentID does not exist on object<SilverStripe\CMS\Model\SiteTree>. Since you implemented __set, maybe consider adding a @property annotation.

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

<?php

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

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

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

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

}

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

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

See also the PhpDoc documentation for @property.

Loading history...
1608
1609
        // DataObject::fieldExists only checks the current class, not the hierarchy
1610
        // This allows the CMS to set the correct sort value
1611
        if ($newItem->castingHelper('Sort')) {
1612
            $newItem->Sort = DB::prepared_query('SELECT MAX("Sort") FROM "SiteTree" WHERE "ParentID" = ?', array($parentID))->value() + 1;
1613
        }
1614
1615
        if ($setID) {
1616
            $newItem->ID = $id;
1617
        }
1618
1619
        # Some modules like subsites add extra fields that need to be set when the new item is created
1620
        $this->extend('augmentNewSiteTreeItem', $newItem);
1621
1622
        return $newItem;
1623
    }
1624
1625
    /**
1626
     * Actually perform the publication step
1627
     *
1628
     * @param Versioned|DataObject $record
1629
     * @return mixed
1630
     */
1631
    public function performPublish($record)
1632
    {
1633
        if ($record && !$record->canPublish()) {
0 ignored issues
show
Bug introduced by
The method canPublish does only exist in SilverStripe\Versioned\Versioned, but not in SilverStripe\ORM\DataObject.

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

Let’s take a look at an example:

class A
{
    public function foo() { }
}

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

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

Available Fixes

  1. Add an additional type-check:

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

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

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

Let’s take a look at an example:

class A
{
    public function foo() { }
}

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

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

Available Fixes

  1. Add an additional type-check:

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

    function someFunction(B $x) { /** ... */ }
    
Loading history...
1638
    }
1639
1640
    /**
1641
     * Reverts a page by publishing it to live.
1642
     * Use {@link restorepage()} if you want to restore a page
1643
     * which was deleted from draft without publishing.
1644
     *
1645
     * @uses SiteTree->doRevertToLive()
1646
     *
1647
     * @param array $data
1648
     * @param Form $form
1649
     * @return HTTPResponse
1650
     * @throws HTTPResponse_Exception
1651
     */
1652
    public function revert($data, $form)
0 ignored issues
show
Unused Code introduced by
The parameter $form is not used and could be removed.

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

Loading history...
1653
    {
1654
        if (!isset($data['ID'])) {
1655
            throw new HTTPResponse_Exception("Please pass an ID in the form content", 400);
1656
        }
1657
1658
        $id = (int) $data['ID'];
1659
        $restoredPage = Versioned::get_latest_version(SiteTree::class, $id);
1660
        if (!$restoredPage) {
1661
            throw new HTTPResponse_Exception("SiteTree #$id not found", 400);
1662
        }
1663
1664
        /** @var SiteTree $record */
1665
        $record = Versioned::get_one_by_stage('SilverStripe\\CMS\\Model\\SiteTree', 'Live', array(
1666
            '"SiteTree_Live"."ID"' => $id
1667
        ));
1668
1669
        // a user can restore a page without publication rights, as it just adds a new draft state
1670
        // (this action should just be available when page has been "deleted from draft")
1671
        if ($record && !$record->canEdit()) {
1672
            return Security::permissionFailure($this);
1673
        }
1674
        if (!$record || !$record->ID) {
1675
            throw new HTTPResponse_Exception("Bad record ID #$id", 404);
1676
        }
1677
1678
        $record->doRevertToLive();
0 ignored issues
show
Documentation Bug introduced by
The method doRevertToLive does not exist on object<SilverStripe\CMS\Model\SiteTree>? Since you implemented __call, maybe consider adding a @method annotation.

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

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

class ParentClass {
    private $data = array();

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

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

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
1679
1680
        $this->getResponse()->addHeader(
1681
            'X-Status',
1682
            rawurlencode(_t(
1683
                'CMSMain.RESTORED',
1684
                "Restored '{title}' successfully",
1685
                'Param %s is a title',
1686
                array('title' => $record->Title)
1687
            ))
1688
        );
1689
1690
        return $this->getResponseNegotiator()->respond($this->getRequest());
1691
    }
1692
1693
    /**
1694
     * Delete the current page from draft stage.
1695
     *
1696
     * @see deletefromlive()
1697
     *
1698
     * @param array $data
1699
     * @param Form $form
1700
     * @return HTTPResponse
1701
     * @throws HTTPResponse_Exception
1702
     */
1703 View Code Duplication
    public function delete($data, $form)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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

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

Loading history...
1704
    {
1705
        $id = $data['ID'];
1706
        $record = SiteTree::get()->byID($id);
1707
        if ($record && !$record->canDelete()) {
1708
            return Security::permissionFailure();
1709
        }
1710
        if (!$record || !$record->ID) {
1711
            throw new HTTPResponse_Exception("Bad record ID #$id", 404);
1712
        }
1713
1714
        // Delete record
1715
        $record->delete();
1716
1717
        $this->getResponse()->addHeader(
1718
            'X-Status',
1719
            rawurlencode(sprintf(_t('CMSMain.REMOVEDPAGEFROMDRAFT', "Removed '%s' from the draft site"), $record->Title))
1720
        );
1721
1722
        // Even if the record has been deleted from stage and live, it can be viewed in "archive mode"
1723
        return $this->getResponseNegotiator()->respond($this->getRequest());
1724
    }
1725
1726
    /**
1727
     * Delete this page from both live and stage
1728
     *
1729
     * @param array $data
1730
     * @param Form $form
1731
     * @return HTTPResponse
1732
     * @throws HTTPResponse_Exception
1733
     */
1734 View Code Duplication
    public function archive($data, $form)
0 ignored issues
show
Unused Code introduced by
The parameter $form is not used and could be removed.

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

Loading history...
Duplication introduced by
This method seems to be duplicated in your project.

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

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

Loading history...
1735
    {
1736
        $id = $data['ID'];
1737
        /** @var SiteTree $record */
1738
        $record = SiteTree::get()->byID($id);
1739
        if (!$record || !$record->exists()) {
1740
            throw new HTTPResponse_Exception("Bad record ID #$id", 404);
1741
        }
1742
        if (!$record->canArchive()) {
0 ignored issues
show
Documentation Bug introduced by
The method canArchive does not exist on object<SilverStripe\CMS\Model\SiteTree>? Since you implemented __call, maybe consider adding a @method annotation.

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

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

class ParentClass {
    private $data = array();

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

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

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
1743
            return Security::permissionFailure();
1744
        }
1745
1746
        // Archive record
1747
        $record->doArchive();
0 ignored issues
show
Documentation Bug introduced by
The method doArchive does not exist on object<SilverStripe\CMS\Model\SiteTree>? Since you implemented __call, maybe consider adding a @method annotation.

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

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

class ParentClass {
    private $data = array();

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

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

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
1748
1749
        $this->getResponse()->addHeader(
1750
            'X-Status',
1751
            rawurlencode(sprintf(_t('CMSMain.ARCHIVEDPAGE', "Archived page '%s'"), $record->Title))
1752
        );
1753
1754
        // Even if the record has been deleted from stage and live, it can be viewed in "archive mode"
1755
        return $this->getResponseNegotiator()->respond($this->getRequest());
1756
    }
1757
1758
    public function publish($data, $form)
1759
    {
1760
        $data['publish'] = '1';
1761
1762
        return $this->save($data, $form);
1763
    }
1764
1765
    public function unpublish($data, $form)
0 ignored issues
show
Unused Code introduced by
The parameter $form is not used and could be removed.

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

Loading history...
1766
    {
1767
        $className = $this->stat('tree_class');
1768
        /** @var SiteTree $record */
1769
        $record = DataObject::get_by_id($className, $data['ID']);
1770
1771
        if ($record && !$record->canUnpublish()) {
0 ignored issues
show
Documentation Bug introduced by
The method canUnpublish does not exist on object<SilverStripe\CMS\Model\SiteTree>? Since you implemented __call, maybe consider adding a @method annotation.

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

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

class ParentClass {
    private $data = array();

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

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

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
1772
            return Security::permissionFailure($this);
1773
        }
1774
        if (!$record || !$record->ID) {
1775
            throw new HTTPResponse_Exception("Bad record ID #" . (int)$data['ID'], 404);
1776
        }
1777
1778
        $record->doUnpublish();
0 ignored issues
show
Documentation Bug introduced by
The method doUnpublish does not exist on object<SilverStripe\CMS\Model\SiteTree>? Since you implemented __call, maybe consider adding a @method annotation.

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

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

class ParentClass {
    private $data = array();

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

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

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
1779
1780
        $this->getResponse()->addHeader(
1781
            'X-Status',
1782
            rawurlencode(_t('CMSMain.REMOVEDPAGE', "Removed '{title}' from the published site", array('title' => $record->Title)))
1783
        );
1784
1785
        return $this->getResponseNegotiator()->respond($this->getRequest());
1786
    }
1787
1788
    /**
1789
     * @return HTTPResponse
1790
     */
1791
    public function rollback()
1792
    {
1793
        return $this->doRollback(array(
1794
            'ID' => $this->currentPageID(),
1795
            'Version' => $this->getRequest()->param('VersionID')
1796
        ), null);
0 ignored issues
show
Documentation introduced by
null is of type null, but the function expects a object<SilverStripe\Forms\Form>.

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1797
    }
1798
1799
    /**
1800
     * Rolls a site back to a given version ID
1801
     *
1802
     * @param array $data
1803
     * @param Form $form
1804
     * @return HTTPResponse
1805
     */
1806
    public function doRollback($data, $form)
0 ignored issues
show
Unused Code introduced by
The parameter $form is not used and could be removed.

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

Loading history...
1807
    {
1808
        $this->extend('onBeforeRollback', $data['ID']);
1809
1810
        $id = (isset($data['ID'])) ? (int) $data['ID'] : null;
1811
        $version = (isset($data['Version'])) ? (int) $data['Version'] : null;
1812
1813
        /** @var DataObject|Versioned $record */
1814
        $record = DataObject::get_by_id($this->stat('tree_class'), $id);
1815
        if ($record && !$record->canEdit()) {
0 ignored issues
show
Bug introduced by
The method canEdit does only exist in SilverStripe\ORM\DataObject, but not in SilverStripe\Versioned\Versioned.

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

Let’s take a look at an example:

class A
{
    public function foo() { }
}

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

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

Available Fixes

  1. Add an additional type-check:

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

    function someFunction(B $x) { /** ... */ }
    
Loading history...
1816
            return Security::permissionFailure($this);
1817
        }
1818
1819
        if ($version) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $version of type integer|null is loosely compared to true; this is ambiguous if the integer can be zero. You might want to explicitly use !== null instead.

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

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
1820
            $record->doRollbackTo($version);
0 ignored issues
show
Bug introduced by
The method doRollbackTo does only exist in SilverStripe\Versioned\Versioned, but not in SilverStripe\ORM\DataObject.

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

Let’s take a look at an example:

class A
{
    public function foo() { }
}

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

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

Available Fixes

  1. Add an additional type-check:

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

    function someFunction(B $x) { /** ... */ }
    
Loading history...
1821
            $message = _t(
1822
                'CMSMain.ROLLEDBACKVERSIONv2',
1823
                "Rolled back to version #%d.",
1824
                array('version' => $data['Version'])
1825
            );
1826
        } else {
1827
            $record->doRevertToLive();
0 ignored issues
show
Bug introduced by
The method doRevertToLive does only exist in SilverStripe\Versioned\Versioned, but not in SilverStripe\ORM\DataObject.

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

Let’s take a look at an example:

class A
{
    public function foo() { }
}

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

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

Available Fixes

  1. Add an additional type-check:

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

    function someFunction(B $x) { /** ... */ }
    
Loading history...
1828
            $message = _t(
1829
                'CMSMain.ROLLEDBACKPUBv2',
1830
                "Rolled back to published version."
1831
            );
1832
        }
1833
1834
        $this->getResponse()->addHeader('X-Status', rawurlencode($message));
1835
1836
        // Can be used in different contexts: In normal page edit view, in which case the redirect won't have any effect.
1837
        // Or in history view, in which case a revert causes the CMS to re-load the edit view.
1838
        // The X-Pjax header forces a "full" content refresh on redirect.
1839
        $url = Controller::join_links(CMSPageEditController::singleton()->Link('show'), $record->ID);
1840
        $this->getResponse()->addHeader('X-ControllerURL', $url);
1841
        $this->getRequest()->addHeader('X-Pjax', 'Content');
1842
        $this->getResponse()->addHeader('X-Pjax', 'Content');
1843
1844
        return $this->getResponseNegotiator()->respond($this->getRequest());
1845
    }
1846
1847
    /**
1848
     * Batch Actions Handler
1849
     */
1850
    public function batchactions()
1851
    {
1852
        return new CMSBatchActionHandler($this, 'batchactions');
1853
    }
1854
1855
    public function BatchActionParameters()
1856
    {
1857
        $batchActions = CMSBatchActionHandler::config()->batch_actions;
1858
1859
        $forms = array();
1860
        foreach ($batchActions as $urlSegment => $batchAction) {
1861
            $SNG_action = singleton($batchAction);
1862
            if ($SNG_action->canView() && $fieldset = $SNG_action->getParameterFields()) {
1863
                $formHtml = '';
1864
                /** @var FormField $field */
1865
                foreach ($fieldset as $field) {
1866
                    $formHtml .= $field->Field();
1867
                }
1868
                $forms[$urlSegment] = $formHtml;
1869
            }
1870
        }
1871
        $pageHtml = '';
1872
        foreach ($forms as $urlSegment => $html) {
1873
            $pageHtml .= "<div class=\"params\" id=\"BatchActionParameters_$urlSegment\">$html</div>\n\n";
1874
        }
1875
        return new LiteralField("BatchActionParameters", '<div id="BatchActionParameters" style="display:none">'.$pageHtml.'</div>');
1876
    }
1877
    /**
1878
     * Returns a list of batch actions
1879
     */
1880
    public function BatchActionList()
1881
    {
1882
        return $this->batchactions()->batchActionList();
1883
    }
1884
1885
    public function publishall($request)
1886
    {
1887
        if (!Permission::check('ADMIN')) {
1888
            return Security::permissionFailure($this);
1889
        }
1890
1891
        increase_time_limit_to();
1892
        increase_memory_limit_to();
1893
1894
        $response = "";
1895
1896
        if (isset($this->requestParams['confirm'])) {
1897
            // Protect against CSRF on destructive action
1898
            if (!SecurityToken::inst()->checkRequest($request)) {
1899
                return $this->httpError(400);
1900
            }
1901
1902
            $start = 0;
1903
            $pages = SiteTree::get()->limit("$start,30");
1904
            $count = 0;
1905
            while ($pages) {
1906
                /** @var SiteTree $page */
1907
                foreach ($pages as $page) {
1908
                    if ($page && !$page->canPublish()) {
1909
                        return Security::permissionFailure($this);
1910
                    }
1911
1912
                    $page->publishRecursive();
1913
                    $page->destroy();
1914
                    unset($page);
1915
                    $count++;
1916
                    $response .= "<li>$count</li>";
1917
                }
1918
                if ($pages->count() > 29) {
1919
                    $start += 30;
1920
                    $pages = SiteTree::get()->limit("$start,30");
1921
                } else {
1922
                    break;
1923
                }
1924
            }
1925
            $response .= _t('CMSMain.PUBPAGES', "Done: Published {count} pages", array('count' => $count));
1926
        } else {
1927
            $token = SecurityToken::inst();
1928
            $fields = new FieldList();
1929
            $token->updateFieldSet($fields);
1930
            $tokenField = $fields->first();
1931
            $tokenHtml = ($tokenField) ? $tokenField->FieldHolder() : '';
1932
            $publishAllDescription = _t(
1933
                'CMSMain.PUBALLFUN2',
1934
                'Pressing this button will do the equivalent of going to every page and pressing "publish".  '
1935
                . 'It\'s intended to be used after there have been massive edits of the content, such as when '
1936
                . 'the site was first built.'
1937
            );
1938
            $response .= '<h1>' . _t('CMSMain.PUBALLFUN', '"Publish All" functionality') . '</h1>
1939
				<p>' . $publishAllDescription . '</p>
1940
				<form method="post" action="publishall">
1941
					<input type="submit" name="confirm" value="'
1942
                    . _t('CMSMain.PUBALLCONFIRM', "Please publish every page in the site, copying content stage to live", 'Confirmation button') .'" />'
1943
                    . $tokenHtml .
1944
                '</form>';
1945
        }
1946
1947
        return $response;
1948
    }
1949
1950
    /**
1951
     * Restore a completely deleted page from the SiteTree_versions table.
1952
     *
1953
     * @param array $data
1954
     * @param Form $form
1955
     * @return HTTPResponse
1956
     */
1957
    public function restore($data, $form)
0 ignored issues
show
Unused Code introduced by
The parameter $form is not used and could be removed.

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

Loading history...
1958
    {
1959
        if (!isset($data['ID']) || !is_numeric($data['ID'])) {
1960
            return new HTTPResponse("Please pass an ID in the form content", 400);
1961
        }
1962
1963
        $id = (int)$data['ID'];
1964
        /** @var SiteTree $restoredPage */
1965
        $restoredPage = Versioned::get_latest_version(SiteTree::class, $id);
1966
        if (!$restoredPage) {
1967
            return new HTTPResponse("SiteTree #$id not found", 400);
1968
        }
1969
1970
        $restoredPage = $restoredPage->doRestoreToStage();
1971
1972
        $this->getResponse()->addHeader(
1973
            'X-Status',
1974
            rawurlencode(_t(
1975
                'CMSMain.RESTORED',
1976
                "Restored '{title}' successfully",
1977
                array('title' => $restoredPage->Title)
1978
            ))
1979
        );
1980
1981
        return $this->getResponseNegotiator()->respond($this->getRequest());
1982
    }
1983
1984
    public function duplicate($request)
1985
    {
1986
        // Protect against CSRF on destructive action
1987
        if (!SecurityToken::inst()->checkRequest($request)) {
1988
            return $this->httpError(400);
1989
        }
1990
1991
        if (($id = $this->urlParams['ID']) && is_numeric($id)) {
1992
            /** @var SiteTree $page */
1993
            $page = SiteTree::get()->byID($id);
1994 View Code Duplication
            if ($page && (!$page->canEdit() || !$page->canCreate(null, array('Parent' => $page->Parent())))) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
1995
                return Security::permissionFailure($this);
1996
            }
1997
            if (!$page || !$page->ID) {
1998
                throw new HTTPResponse_Exception("Bad record ID #$id", 404);
1999
            }
2000
2001
            $newPage = $page->duplicate();
2002
2003
            // ParentID can be hard-set in the URL.  This is useful for pages with multiple parents
2004
            if (isset($_GET['parentID']) && is_numeric($_GET['parentID'])) {
2005
                $newPage->ParentID = $_GET['parentID'];
2006
                $newPage->write();
2007
            }
2008
2009
            $this->getResponse()->addHeader(
2010
                'X-Status',
2011
                rawurlencode(_t(
2012
                    'CMSMain.DUPLICATED',
2013
                    "Duplicated '{title}' successfully",
2014
                    array('title' => $newPage->Title)
2015
                ))
2016
            );
2017
            $url = Controller::join_links(CMSPageEditController::singleton()->Link('show'), $newPage->ID);
2018
            $this->getResponse()->addHeader('X-ControllerURL', $url);
2019
            $this->getRequest()->addHeader('X-Pjax', 'Content');
2020
            $this->getResponse()->addHeader('X-Pjax', 'Content');
2021
2022
            return $this->getResponseNegotiator()->respond($this->getRequest());
2023
        } else {
2024
            return new HTTPResponse("CMSMain::duplicate() Bad ID: '$id'", 400);
2025
        }
2026
    }
2027
2028
    public function duplicatewithchildren($request)
2029
    {
2030
        // Protect against CSRF on destructive action
2031
        if (!SecurityToken::inst()->checkRequest($request)) {
2032
            return $this->httpError(400);
2033
        }
2034
        increase_time_limit_to();
2035
        if (($id = $this->urlParams['ID']) && is_numeric($id)) {
2036
            /** @var SiteTree $page */
2037
            $page = SiteTree::get()->byID($id);
2038 View Code Duplication
            if ($page && (!$page->canEdit() || !$page->canCreate(null, array('Parent' => $page->Parent())))) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
2039
                return Security::permissionFailure($this);
2040
            }
2041
            if (!$page || !$page->ID) {
2042
                throw new HTTPResponse_Exception("Bad record ID #$id", 404);
2043
            }
2044
2045
            $newPage = $page->duplicateWithChildren();
2046
2047
            $this->getResponse()->addHeader(
2048
                'X-Status',
2049
                rawurlencode(_t(
2050
                    'CMSMain.DUPLICATEDWITHCHILDREN',
2051
                    "Duplicated '{title}' and children successfully",
2052
                    array('title' => $newPage->Title)
2053
                ))
2054
            );
2055
            $url = Controller::join_links(CMSPageEditController::singleton()->Link('show'), $newPage->ID);
2056
            $this->getResponse()->addHeader('X-ControllerURL', $url);
2057
            $this->getRequest()->addHeader('X-Pjax', 'Content');
2058
            $this->getResponse()->addHeader('X-Pjax', 'Content');
2059
2060
            return $this->getResponseNegotiator()->respond($this->getRequest());
2061
        } else {
2062
            return new HTTPResponse("CMSMain::duplicatewithchildren() Bad ID: '$id'", 400);
2063
        }
2064
    }
2065
2066
    public function providePermissions()
2067
    {
2068
        $title = CMSPagesController::menu_title();
2069
        return array(
2070
            "CMS_ACCESS_CMSMain" => array(
2071
                'name' => _t('CMSMain.ACCESS', "Access to '{title}' section", array('title' => $title)),
2072
                'category' => _t('Permission.CMS_ACCESS_CATEGORY', 'CMS Access'),
2073
                'help' => _t(
2074
                    'CMSMain.ACCESS_HELP',
2075
                    'Allow viewing of the section containing page tree and content. View and edit permissions can be handled through page specific dropdowns, as well as the separate "Content permissions".'
2076
                ),
2077
                'sort' => -99 // below "CMS_ACCESS_LeftAndMain", but above everything else
2078
            )
2079
        );
2080
    }
2081
2082
    /**
2083
     * Get title for root CMS node
2084
     *
2085
     * @return string
2086
     */
2087
    protected function getCMSTreeTitle()
2088
    {
2089
        $rootTitle = SiteConfig::current_site_config()->Title;
2090
        $this->extend('updateCMSTreeTitle', $rootTitle);
2091
        return $rootTitle;
2092
    }
2093
}
2094