Completed
Pull Request — master (#1665)
by Damian
02:26
created

CMSMain::ViewState()   A

Complexity

Conditions 4
Paths 6

Size

Total Lines 11
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 11
rs 9.2
c 0
b 0
f 0
cc 4
eloc 9
nc 6
nop 0
1
<?php
2
3
namespace SilverStripe\CMS\Controllers;
4
5
use SilverStripe\Admin\AdminRootController;
6
use SilverStripe\Admin\CMSBatchActionHandler;
7
use SilverStripe\Admin\CMSPreviewable;
8
use SilverStripe\Admin\LeftAndMain;
9
use SilverStripe\CMS\BatchActions\CMSBatchAction_Archive;
10
use SilverStripe\CMS\BatchActions\CMSBatchAction_Publish;
11
use SilverStripe\CMS\BatchActions\CMSBatchAction_Restore;
12
use SilverStripe\CMS\BatchActions\CMSBatchAction_Unpublish;
13
use SilverStripe\CMS\Model\CurrentPageIdentifier;
14
use SilverStripe\CMS\Model\RedirectorPage;
15
use SilverStripe\CMS\Model\SiteTree;
16
use SilverStripe\Control\Controller;
17
use SilverStripe\Control\Director;
18
use SilverStripe\Control\Session;
19
use SilverStripe\Control\HTTPRequest;
20
use SilverStripe\Control\HTTPResponse;
21
use SilverStripe\Control\HTTPResponse_Exception;
22
use SilverStripe\Core\Convert;
23
use SilverStripe\Core\Injector\Injector;
24
use SilverStripe\Core\Cache;
25
use SilverStripe\Forms\DateField;
26
use SilverStripe\Forms\DropdownField;
27
use SilverStripe\Forms\FieldGroup;
28
use SilverStripe\Forms\FieldList;
29
use SilverStripe\Forms\Form;
30
use SilverStripe\Forms\FormAction;
31
use SilverStripe\Forms\FormField;
32
use SilverStripe\Forms\GridField\GridField;
33
use SilverStripe\Forms\GridField\GridFieldConfig;
34
use SilverStripe\Forms\GridField\GridFieldDataColumns;
35
use SilverStripe\Forms\GridField\GridFieldLevelup;
36
use SilverStripe\Forms\GridField\GridFieldPaginator;
37
use SilverStripe\Forms\GridField\GridFieldSortableHeader;
38
use SilverStripe\Forms\HiddenField;
39
use SilverStripe\Forms\LabelField;
40
use SilverStripe\Forms\LiteralField;
41
use SilverStripe\Forms\RequiredFields;
42
use SilverStripe\Forms\ResetFormAction;
43
use SilverStripe\Forms\TabSet;
44
use SilverStripe\Forms\TextField;
45
use SilverStripe\ORM\ArrayList;
46
use SilverStripe\ORM\DataList;
47
use SilverStripe\ORM\DataObject;
48
use SilverStripe\ORM\DB;
49
use SilverStripe\ORM\FieldType\DBHTMLText;
50
use SilverStripe\ORM\HiddenClass;
51
use SilverStripe\ORM\SS_List;
52
use SilverStripe\ORM\Versioning\Versioned;
53
use SilverStripe\Security\Member;
54
use SilverStripe\Security\Permission;
55
use SilverStripe\Security\PermissionProvider;
56
use SilverStripe\Security\Security;
57
use SilverStripe\Security\SecurityToken;
58
use SilverStripe\View\ArrayData;
59
use SilverStripe\View\Requirements;
60
use Translatable;
61
use Page;
62
use Zend_Cache;
63
use InvalidArgumentException;
64
65
66
/**
67
 * The main "content" area of the CMS.
68
 *
69
 * This class creates a 2-frame layout - left-tree and right-form - to sit beneath the main
70
 * admin menu.
71
 *
72
 * @todo Create some base classes to contain the generic functionality that will be replicated.
73
 *
74
 * @mixin LeftAndMainPageIconsExtension
75
 */
76
class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionProvider {
77
78
	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...
79
80
	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...
81
82
	// Maintain a lower priority than other administration sections
83
	// so that Director does not think they are actions of CMSMain
84
	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...
85
86
	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...
87
88
	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...
89
90
	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...
91
92
	private static $subitem_class = Member::class;
93
94
	private static $session_namespace = 'SilverStripe\\CMS\\Controllers\\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...
95
96
	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...
97
98
	/**
99
	 * Amount of results showing on a single page.
100
	 *
101
	 * @config
102
	 * @var int
103
	 */
104
	private static $page_length = 15;
105
106
	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...
107
		'archive',
108
		'deleteitems',
109
		'DeleteItemsForm',
110
		'dialog',
111
		'duplicate',
112
		'duplicatewithchildren',
113
		'publishall',
114
		'publishitems',
115
		'PublishItemsForm',
116
		'submit',
117
		'EditForm',
118
		'SearchForm',
119
		'SiteTreeAsUL',
120
		'getshowdeletedsubtree',
121
		'batchactions',
122
		'treeview',
123
		'listview',
124
		'ListViewForm',
125
		'childfilter',
126
	);
127
128
	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...
129
		'TreeIsFiltered' => 'Boolean',
130
		'AddForm' => 'HTMLFragment',
131
		'LinkPages' => 'Text',
132
		'Link' => 'Text',
133
		'ListViewForm' => 'HTMLFragment',
134
		'ExtraTreeTools' => 'HTMLFragment',
135
		'PageList' => 'HTMLFragment',
136
		'PageListSidebar' => 'HTMLFragment',
137
		'SiteTreeHints' => 'HTMLFragment',
138
		'SecurityID' => 'Text',
139
		'SiteTreeAsUL' => 'HTMLFragment',
140
	);
141
142
	public function init() {
143
		// set reading lang
144
		if(SiteTree::has_extension('Translatable') && !$this->getRequest()->isAjax()) {
145
			Translatable::choose_site_locale(array_keys(Translatable::get_existing_content_languages('SilverStripe\\CMS\\Model\\SiteTree')));
146
		}
147
148
		parent::init();
149
150
		Requirements::javascript(CMS_DIR . '/client/dist/js/bundle.js');
151
		Requirements::javascript(CMS_DIR . '/client/dist/js/SilverStripeNavigator.js');
152
		Requirements::css(CMS_DIR . '/client/dist/styles/bundle.css');
153
		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...
154
		Requirements::add_i18n_javascript(CMS_DIR . '/client/lang', false, true);
155
156
		CMSBatchActionHandler::register('restore', CMSBatchAction_Restore::class);
157
		CMSBatchActionHandler::register('archive', CMSBatchAction_Archive::class);
158
		CMSBatchActionHandler::register('unpublish', CMSBatchAction_Unpublish::class);
159
		CMSBatchActionHandler::register('publish', CMSBatchAction_Publish::class);
160
	}
161
162
	public function index($request) {
163
		// In case we're not showing a specific record, explicitly remove any session state,
164
		// to avoid it being highlighted in the tree, and causing an edit form to show.
165
		if(!$request->param('Action')) {
166
			$this->setCurrentPageID(null);
167
		}
168
169
		return parent::index($request);
170
	}
171
172
	public function getResponseNegotiator() {
173
		$negotiator = parent::getResponseNegotiator();
174
175
		// ListViewForm
176
		$negotiator->setCallback('ListViewForm', function()  {
177
			return $this->ListViewForm()->forTemplate();
178
		});
179
180
		// PageList view
181
		$negotiator->setCallback('Content-PageList', function () {
182
			return $this->PageList()->forTemplate();
183
		});
184
185
		// PageList view for edit controller
186
		$negotiator->setCallback('Content-PageList-Sidebar', function() {
187
			return $this->PageListSidebar()->forTemplate();
188
		});
189
190
		return $negotiator;
191
	}
192
193
	/**
194
	 * Get pages listing area
195
	 *
196
	 * @return DBHTMLText
197
	 */
198
	public function PageList() {
199
		return $this->renderWith($this->getTemplatesWithSuffix('_PageList'));
200
	}
201
202
	/**
203
	 * Page list view for edit-form
204
	 *
205
	 * @return DBHTMLText
206
	 */
207
	public function PageListSidebar() {
208
		return $this->renderWith($this->getTemplatesWithSuffix('_PageList_Sidebar'));
209
	}
210
211
	/**
212
	 * If this is set to true, the "switchView" context in the
213
	 * template is shown, with links to the staging and publish site.
214
	 *
215
	 * @return boolean
216
	 */
217
	public function ShowSwitchView() {
218
		return true;
219
	}
220
221
	/**
222
	 * Overloads the LeftAndMain::ShowView. Allows to pass a page as a parameter, so we are able
223
	 * to switch view also for archived versions.
224
	 *
225
	 * @param SiteTree $page
226
	 * @return array
227
	 */
228
	public function SwitchView($page = null) {
229
		if(!$page) {
230
			$page = $this->currentPage();
231
		}
232
233
		if($page) {
234
			$nav = SilverStripeNavigator::get_for_record($page);
235
			return $nav['items'];
236
		}
237
	}
238
239
	//------------------------------------------------------------------------------------------//
240
	// Main controllers
241
242
	//------------------------------------------------------------------------------------------//
243
	// Main UI components
244
245
	/**
246
	 * Override {@link LeftAndMain} Link to allow blank URL segment for CMSMain.
247
	 *
248
	 * @param string|null $action Action to link to.
249
	 * @return string
250
	 */
251
	public function Link($action = null) {
252
		$link = Controller::join_links(
253
			AdminRootController::admin_url(),
254
			$this->stat('url_segment'), // in case we want to change the segment
255
			'/', // trailing slash needed if $action is null!
256
			"$action"
257
		);
258
		$this->extend('updateLink', $link);
259
		return $link;
260
	}
261
262
	public function LinkPages() {
263
		return CMSPagesController::singleton()->Link();
264
	}
265
266
	public function LinkPagesWithSearch() {
267
		return $this->LinkWithSearch($this->LinkPages());
268
	}
269
270
	/**
271
	 * Get link to tree view
272
	 *
273
	 * @return string
274
	 */
275
	public function LinkTreeView() {
276
		// Tree view is just default link to main pages section (no /treeview suffix)
277
		return $this->LinkWithSearch(CMSMain::singleton()->Link());
278
	}
279
280
	/**
281
	 * Get link to list view
282
	 *
283
	 * @return string
284
	 */
285
	public function LinkListView() {
286
		// Note : Force redirect to top level page controller
287
		return $this->LinkWithSearch(CMSMain::singleton()->Link('listview'));
288
	}
289
290 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...
291
		if(!$id) {
292
			$id = $this->currentPageID();
293
		}
294
		return $this->LinkWithSearch(
295
			Controller::join_links(CMSPageEditController::singleton()->Link('show'), $id)
296
		);
297
	}
298
299 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...
300
		if($id = $this->currentPageID()) {
301
			return $this->LinkWithSearch(
302
				Controller::join_links(CMSPageSettingsController::singleton()->Link('show'), $id)
303
			);
304
		} else {
305
			return null;
306
		}
307
	}
308
309 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...
310
		if($id = $this->currentPageID()) {
311
			return $this->LinkWithSearch(
312
				Controller::join_links(CMSPageHistoryController::singleton()->Link('show'), $id)
313
			);
314
		} else {
315
			return null;
316
		}
317
	}
318
319
	public function LinkWithSearch($link) {
320
		// Whitelist to avoid side effects
321
		$params = array(
322
			'q' => (array)$this->getRequest()->getVar('q'),
323
			'ParentID' => $this->getRequest()->getVar('ParentID')
324
		);
325
		$link = Controller::join_links(
326
			$link,
327
			array_filter(array_values($params)) ? '?' . http_build_query($params) : null
328
		);
329
		$this->extend('updateLinkWithSearch', $link);
330
		return $link;
331
	}
332
333
	public function LinkPageAdd($extra = null, $placeholders = null) {
334
		$link = CMSPageAddController::singleton()->Link();
335
		$this->extend('updateLinkPageAdd', $link);
336
337
		if($extra) {
338
			$link = Controller::join_links ($link, $extra);
339
		}
340
341
		if($placeholders) {
342
			$link .= (strpos($link, '?') === false ? "?$placeholders" : "&amp;$placeholders");
343
		}
344
345
		return $link;
346
	}
347
348
	/**
349
	 * @return string
350
	 */
351
	public function LinkPreview() {
352
		$record = $this->getRecord($this->currentPageID());
353
		$baseLink = Director::absoluteBaseURL();
354
		if ($record && $record instanceof Page) {
355
			// if we are an external redirector don't show a link
356
			if ($record instanceof RedirectorPage && $record->RedirectionType == 'External') {
357
				$baseLink = false;
358
			}
359
			else {
360
				$baseLink = $record->Link('?stage=Stage');
361
			}
362
		}
363
		return $baseLink;
364
	}
365
366
	/**
367
	 * Return the entire site tree as a nested set of ULs
368
	 */
369
	public function SiteTreeAsUL() {
370
		// Pre-cache sitetree version numbers for querying efficiency
371
		Versioned::prepopulate_versionnumber_cache(SiteTree::class, "Stage");
372
		Versioned::prepopulate_versionnumber_cache(SiteTree::class, "Live");
373
		$html = $this->getSiteTreeFor($this->stat('tree_class'));
374
375
		$this->extend('updateSiteTreeAsUL', $html);
376
377
		return $html;
378
	}
379
380
	/**
381
	 * @return boolean
382
	 */
383
	public function TreeIsFiltered() {
384
		$query = $this->getRequest()->getVar('q');
385
386
		if (!$query || (count($query) === 1 && isset($query['FilterClass']) && $query['FilterClass'] === 'SilverStripe\\CMS\\Controllers\\CMSSiteTreeFilter_Search')) {
387
			return false;
388
		}
389
390
		return true;
391
	}
392
393
	public function ExtraTreeTools() {
394
		$html = '';
395
		$this->extend('updateExtraTreeTools', $html);
396
		return $html;
397
	}
398
399
	/**
400
	 * Returns a Form for page searching for use in templates.
401
	 *
402
	 * Can be modified from a decorator by a 'updateSearchForm' method
403
	 *
404
	 * @return Form
405
	 */
406
	public function SearchForm() {
407
		// Create the fields
408
		$content = new TextField('q[Term]', _t('CMSSearch.FILTERLABELTEXT', 'Search'));
409
		$dateFrom = new DateField(
410
			'q[LastEditedFrom]',
411
			_t('CMSSearch.FILTERDATEFROM', 'From')
412
		);
413
		$dateFrom->setConfig('showcalendar', true);
414
		$dateTo = new DateField(
415
			'q[LastEditedTo]',
416
			_t('CMSSearch.FILTERDATETO', 'To')
417
		);
418
		$dateTo->setConfig('showcalendar', true);
419
		$pageFilter = new DropdownField(
420
			'q[FilterClass]',
421
			_t('CMSMain.PAGES', 'Page status'),
422
			CMSSiteTreeFilter::get_all_filters()
423
		);
424
		$pageClasses = new DropdownField(
425
			'q[ClassName]',
426
			_t('CMSMain.PAGETYPEOPT', 'Page type', 'Dropdown for limiting search to a page type'),
427
			$this->getPageTypes()
428
		);
429
		$pageClasses->setEmptyString(_t('CMSMain.PAGETYPEANYOPT','Any'));
430
431
		// Group the Datefields
432
		$dateGroup = new FieldGroup(
433
			$dateFrom,
434
			$dateTo
435
		);
436
		$dateGroup->setTitle(_t('CMSSearch.PAGEFILTERDATEHEADING', 'Last edited'));
437
438
		// view mode
439
		$viewMode = HiddenField::create('view', false, $this->ViewState());
440
441
		// Create the Field list
442
		$fields = new FieldList(
443
			$content,
444
			$pageFilter,
445
			$pageClasses,
446
			$dateGroup,
447
			$viewMode
448
		);
449
450
		// Create the Search and Reset action
451
		$actions = new FieldList(
452
			FormAction::create('doSearch',  _t('CMSMain_left_ss.APPLY_FILTER', 'Search'))
453
				->addExtraClass('ss-ui-action-constructive'),
454
			ResetFormAction::create('clear', _t('CMSMain_left_ss.CLEAR_FILTER', 'Clear'))
455
		);
456
457
		// Use <button> to allow full jQuery UI styling on the all of the Actions
458
		/** @var FormAction $action */
459
		foreach($actions->dataFields() as $action) {
460
			/** @var FormAction $action */
461
			$action->setUseButtonTag(true);
462
		}
463
464
		// Create the form
465
		/** @skipUpgrade */
466
		$form = Form::create($this, 'SearchForm', $fields, $actions)
467
			->addExtraClass('cms-search-form')
468
			->setFormMethod('GET')
469
			->setFormAction($this->Link())
470
			->disableSecurityToken()
471
			->unsetValidator();
472
473
		// Load the form with previously sent search data
474
		$form->loadDataFrom($this->getRequest()->getVars());
475
476
		// Allow decorators to modify the form
477
		$this->extend('updateSearchForm', $form);
478
479
		return $form;
480
	}
481
482
	/**
483
	 * Returns a sorted array suitable for a dropdown with pagetypes and their translated name
484
	 *
485
	 * @return array
486
	 */
487
	protected function getPageTypes() {
488
		$pageTypes = array();
489
		foreach(SiteTree::page_type_classes() as $pageTypeClass) {
490
			$pageTypes[$pageTypeClass] = SiteTree::singleton($pageTypeClass)->i18n_singular_name();
491
		}
492
		asort($pageTypes);
493
		return $pageTypes;
494
	}
495
496
	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...
497
		return $this->getsubtree($this->getRequest());
498
	}
499
500
	/**
501
	 * @param bool $unlinked
502
	 * @return ArrayList
503
	 */
504
	public function Breadcrumbs($unlinked = false) {
505
		$items = parent::Breadcrumbs($unlinked);
506
507
		if($items->count() > 1) {
508
			// Specific to the SiteTree admin section, we never show the cms section and current
509
			// page in the same breadcrumbs block.
510
			$items->shift();
511
		}
512
513
		return $items;
514
	}
515
516
	/**
517
	 * Create serialized JSON string with site tree hints data to be injected into
518
	 * 'data-hints' attribute of root node of jsTree.
519
	 *
520
	 * @return string Serialized JSON
521
	 */
522
	public function SiteTreeHints() {
523
		$classes = SiteTree::page_type_classes();
524
525
	 	$cacheCanCreate = array();
526
	 	foreach($classes as $class) $cacheCanCreate[$class] = singleton($class)->canCreate();
527
528
	 	// Generate basic cache key. Too complex to encompass all variations
529
	 	$cache = Cache::factory('CMSMain_SiteTreeHints');
530
	 	$cacheKey = md5(implode('_', array(Member::currentUserID(), implode(',', $cacheCanCreate), implode(',', $classes))));
531
	 	if($this->getRequest()->getVar('flush')) {
532
	 		$cache->clean(Zend_Cache::CLEANING_MODE_ALL);
533
		}
534
	 	$json = $cache->load($cacheKey);
535
	 	if(!$json) {
536
			$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...
537
			$def['Root']['disallowedChildren'] = array();
538
539
			// Contains all possible classes to support UI controls listing them all,
540
			// such as the "add page here" context menu.
541
			$def['All'] = array();
542
543
			// Identify disallows and set globals
544
			foreach($classes as $class) {
545
				$obj = singleton($class);
546
				if($obj instanceof HiddenClass) continue;
547
548
				// Name item
549
				$def['All'][$class] = array(
550
					'title' => $obj->i18n_singular_name()
551
				);
552
553
				// Check if can be created at the root
554
				$needsPerm = $obj->stat('need_permission');
555
				if(
556
					!$obj->stat('can_be_root')
557
					|| (!array_key_exists($class, $cacheCanCreate) || !$cacheCanCreate[$class])
558
					|| ($needsPerm && !$this->can($needsPerm))
559
				) {
560
					$def['Root']['disallowedChildren'][] = $class;
561
				}
562
563
				// Hint data specific to the class
564
				$def[$class] = array();
565
566
				$defaultChild = $obj->defaultChild();
567
				if($defaultChild !== 'Page' && $defaultChild !== null) {
568
					$def[$class]['defaultChild'] = $defaultChild;
569
				}
570
571
				$defaultParent = $obj->defaultParent();
572
				if ($defaultParent !== 1 && $defaultParent !== null) {
573
					$def[$class]['defaultParent'] = $defaultParent;
574
				}
575
			}
576
577
			$this->extend('updateSiteTreeHints', $def);
578
579
			$json = Convert::raw2json($def);
580
			$cache->save($json, $cacheKey);
581
		}
582
		return $json;
583
	}
584
585
	/**
586
	 * Populates an array of classes in the CMS
587
	 * which allows the user to change the page type.
588
	 *
589
	 * @return SS_List
590
	 */
591
	public function PageTypes() {
592
		$classes = SiteTree::page_type_classes();
593
594
		$result = new ArrayList();
595
596
		foreach($classes as $class) {
597
			$instance = singleton($class);
598
599
			if($instance instanceof HiddenClass) {
600
				continue;
601
			}
602
603
			// skip this type if it is restricted
604
			if($instance->stat('need_permission') && !$this->can(singleton($class)->stat('need_permission'))) {
605
				continue;
606
			}
607
608
			$addAction = $instance->i18n_singular_name();
609
610
			// Get description (convert 'Page' to 'SiteTree' for correct localization lookups)
611
			$i18nClass = ($class == 'Page') ? 'SilverStripe\\CMS\\Model\\SiteTree' : $class;
612
			$description = _t($i18nClass . '.DESCRIPTION');
613
614
			if(!$description) {
615
				$description = $instance->uninherited('description');
616
			}
617
618
			if($class == 'Page' && !$description) {
619
				$description = SiteTree::singleton()->uninherited('description');
620
			}
621
622
			$result->push(new ArrayData(array(
623
				'ClassName' => $class,
624
				'AddAction' => $addAction,
625
				'Description' => $description,
626
				// TODO Sprite support
627
				'IconURL' => $instance->stat('icon'),
628
				'Title' => singleton($class)->i18n_singular_name(),
629
			)));
630
		}
631
632
		$result = $result->sort('AddAction');
633
634
		return $result;
635
	}
636
637
	/**
638
	 * Get a database record to be managed by the CMS.
639
	 *
640
	 * @param int $id Record ID
641
	 * @param int $versionID optional Version id of the given record
642
	 * @return SiteTree
643
	 */
644
 	public function getRecord($id, $versionID = null) {
645
		$treeClass = $this->stat('tree_class');
646
647
		if($id instanceof $treeClass) {
648
			return $id;
649
		}
650
		else if($id && is_numeric($id)) {
651
			$currentStage = Versioned::get_reading_mode();
652
653
			if($this->getRequest()->getVar('Version')) {
654
				$versionID = (int) $this->getRequest()->getVar('Version');
655
			}
656
657
			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...
658
				$record = Versioned::get_version($treeClass, $id, $versionID);
659
			} else {
660
				$record = DataObject::get_by_id($treeClass, $id);
661
			}
662
663
			// Then, try getting a record from the live site
664
			if(!$record) {
665
				// $record = Versioned::get_one_by_stage($treeClass, "Live", "\"$treeClass\".\"ID\" = $id");
666
				Versioned::set_stage(Versioned::LIVE);
667
				singleton($treeClass)->flushCache();
668
669
				$record = DataObject::get_by_id($treeClass, $id);
670
			}
671
672
			// Then, try getting a deleted record
673
			if(!$record) {
674
				$record = Versioned::get_latest_version($treeClass, $id);
675
			}
676
677
			// Don't open a page from a different locale
678
			/** The record's Locale is saved in database in 2.4, and not related with Session,
679
			 *  we should not check their locale matches the Translatable::get_current_locale,
680
			 * 	here as long as we all the HTTPRequest is init with right locale.
681
			 *	This bit breaks the all FileIFrameField functions if the field is used in CMS
682
			 *  and its relevent ajax calles, like loading the tree dropdown for TreeSelectorField.
683
			 */
684
			/* if($record && SiteTree::has_extension('Translatable') && $record->Locale && $record->Locale != Translatable::get_current_locale()) {
685
				$record = null;
686
			}*/
687
688
			// Set the reading mode back to what it was.
689
			Versioned::set_reading_mode($currentStage);
690
691
			return $record;
692
693
		} else if(substr($id,0,3) == 'new') {
694
			return $this->getNewItem($id);
695
		}
696
	}
697
698
	/**
699
	 * @param int $id
700
	 * @param FieldList $fields
701
	 * @return Form
702
	 */
703
	public function getEditForm($id = null, $fields = null) {
704
		if(!$id) $id = $this->currentPageID();
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...
705
		$form = parent::getEditForm($id, $fields);
706
707
		// TODO Duplicate record fetching (see parent implementation)
708
		$record = $this->getRecord($id);
709
		if($record && !$record->canView()) return Security::permissionFailure($this);
0 ignored issues
show
Bug Compatibility introduced by
The expression \SilverStripe\Security\S...rmissionFailure($this); of type SilverStripe\Control\HTTPResponse|null adds the type SilverStripe\Control\HTTPResponse to the return on line 709 which is incompatible with the return type documented by SilverStripe\CMS\Controllers\CMSMain::getEditForm of type SilverStripe\Forms\Form.
Loading history...
710
711
		if(!$fields) $fields = $form->Fields();
712
		$actions = $form->Actions();
0 ignored issues
show
Unused Code introduced by
$actions is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
713
714
		if($record) {
715
			$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...
716
717
			$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...
718
			// Necessary for different subsites
719
			$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...
720
			$fields->push($liveLinkField = new HiddenField("LiveLink"));
721
			$fields->push($stageLinkField = new HiddenField("StageLink"));
722
			$fields->push(new HiddenField("TreeTitle", false, $record->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...
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...
723
724
			if($record->ID && is_numeric( $record->ID ) ) {
725
				$liveLink = $record->getAbsoluteLiveLink();
726
				if($liveLink) $liveLinkField->setValue($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...
727
				if(!$deletedFromStage) {
728
					$stageLink = Controller::join_links($record->AbsoluteLink(), '?stage=Stage');
729
					if($stageLink) $stageLinkField->setValue($stageLink);
730
				}
731
			}
732
733
			// Added in-line to the form, but plucked into different view by LeftAndMain.Preview.js upon load
734
			/** @skipUpgrade */
735
			if($record instanceof CMSPreviewable && !$fields->fieldByName('SilverStripeNavigator')) {
736
				$navField = new LiteralField('SilverStripeNavigator', $this->getSilverStripeNavigator());
737
				$navField->setAllowHTML(true);
738
				$fields->push($navField);
739
			}
740
741
			// getAllCMSActions can be used to completely redefine the action list
742
			if($record->hasMethod('getAllCMSActions')) {
743
				$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...
744
			} else {
745
				$actions = $record->getCMSActions();
746
747
				// Find and remove action menus that have no actions.
748
				if ($actions && $actions->count()) {
749
					/** @var TabSet $tabset */
750
					$tabset = $actions->fieldByName('ActionMenus');
751
					if ($tabset) {
752
						foreach ($tabset->getChildren() as $tab) {
753
							if (!$tab->getChildren()->count()) {
754
								$tabset->removeByName($tab->getName());
755
							}
756
						}
757
					}
758
				}
759
			}
760
761
			// Use <button> to allow full jQuery UI styling
762
			$actionsFlattened = $actions->dataFields();
763
			if($actionsFlattened) {
764
				/** @var FormAction $action */
765
				foreach($actionsFlattened as $action) {
766
					$action->setUseButtonTag(true);
767
				}
768
			}
769
770
			if($record->hasMethod('getCMSValidator')) {
771
				$validator = $record->getCMSValidator();
0 ignored issues
show
Documentation Bug introduced by
The method getCMSValidator 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...
Unused Code introduced by
$validator is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
772
			} else {
773
				$validator = new RequiredFields();
0 ignored issues
show
Unused Code introduced by
$validator is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
774
			}
775
776
			// TODO Can't merge $FormAttributes in template at the moment
777
			$form->addExtraClass('center ' . $this->BaseCSSClasses());
778
			// Set validation exemptions for specific actions
779
			$form->setValidationExemptActions(array('restore', 'revert', 'deletefromlive', 'delete', 'unpublish', 'rollback', 'doRollback'));
780
781
			// Announce the capability so the frontend can decide whether to allow preview or not.
782
			if ($record instanceof CMSPreviewable) {
783
				$form->addExtraClass('cms-previewable');
784
			}
785
			$form->addExtraClass('fill-height flexbox-area-grow');
786
787
			if(!$record->canEdit() || $deletedFromStage) {
788
				$readonlyFields = $form->Fields()->makeReadonly();
789
				$form->setFields($readonlyFields);
790
			}
791
792
			$form->Fields()->setForm($form);
793
794
			$this->extend('updateEditForm', $form);
795
			return $form;
796
		} else if($id) {
797
			$form = Form::create( $this, "EditForm", new FieldList(
798
				new LabelField('PageDoesntExistLabel',_t('CMSMain.PAGENOTEXISTS',"This page doesn't exist"))), new FieldList()
799
			)->setHTMLID('Form_EditForm');
800
			return $form;
801
		}
802
	}
803
804
	/**
805
	 * @param HTTPRequest $request
806
	 * @return string HTML
807
	 */
808
	public function treeview($request) {
809
		return $this->getResponseNegotiator()->respond($request);
810
	}
811
812
	/**
813
	 * @param HTTPRequest $request
814
	 * @return string HTML
815
	 */
816
	public function listview($request) {
817
		return $this->getResponseNegotiator()->respond($request);
818
	}
819
820
	/**
821
	 * @return string
822
	 */
823
	public function ViewState() {
824
		$mode = $this->getRequest()->requestVar('view')
825
			?: $this->getRequest()->param('Action');
826
		switch($mode) {
827
			case 'listview':
828
			case 'treeview':
829
				return $mode;
830
			default:
831
				return 'treeview';
832
		}
833
	}
834
835
	/**
836
	 * Callback to request the list of page types allowed under a given page instance.
837
	 * Provides a slower but more precise response over SiteTreeHints
838
	 *
839
	 * @param HTTPRequest $request
840
	 * @return HTTPResponse
841
	 */
842
	public function childfilter($request) {
843
		// Check valid parent specified
844
		$parentID = $request->requestVar('ParentID');
845
		$parent = SiteTree::get()->byID($parentID);
846
		if(!$parent || !$parent->exists()) return $this->httpError(404);
847
848
		// Build hints specific to this class
849
		// Identify disallows and set globals
850
		$classes = SiteTree::page_type_classes();
851
		$disallowedChildren = array();
852
		foreach($classes as $class) {
853
			$obj = singleton($class);
854
			if($obj instanceof HiddenClass) continue;
855
856
			if(!$obj->canCreate(null, array('Parent' => $parent))) {
857
				$disallowedChildren[] = $class;
858
			}
859
		}
860
861
		$this->extend('updateChildFilter', $disallowedChildren, $parentID);
862
		return $this
863
			->getResponse()
864
			->addHeader('Content-Type', 'application/json; charset=utf-8')
865
			->setBody(Convert::raw2json($disallowedChildren));
866
	}
867
868
	/**
869
	 * Safely reconstruct a selected filter from a given set of query parameters
870
	 *
871
	 * @param array $params Query parameters to use
872
	 * @return CMSSiteTreeFilter The filter class, or null if none present
873
	 * @throws InvalidArgumentException if invalid filter class is passed.
874
	 */
875
	protected function getQueryFilter($params) {
876
		if(empty($params['FilterClass'])) return null;
877
		$filterClass = $params['FilterClass'];
878
		if(!is_subclass_of($filterClass, 'SilverStripe\\CMS\\Controllers\\CMSSiteTreeFilter')) {
879
			throw new InvalidArgumentException("Invalid filter class passed: {$filterClass}");
880
		}
881
		return $filterClass::create($params);
882
	}
883
884
	/**
885
	 * Returns the pages meet a certain criteria as {@see CMSSiteTreeFilter} or the subpages of a parent page
886
	 * defaulting to no filter and show all pages in first level.
887
	 * Doubles as search results, if any search parameters are set through {@link SearchForm()}.
888
	 *
889
	 * @param array $params Search filter criteria
890
	 * @param int $parentID Optional parent node to filter on (can't be combined with other search criteria)
891
	 * @return SS_List
892
	 * @throws InvalidArgumentException if invalid filter class is passed.
893
	 */
894
	public function getList($params = array(), $parentID = 0) {
895
		if($filter = $this->getQueryFilter($params)) {
896
			return $filter->getFilteredPages();
897
		} else {
898
			$list = DataList::create($this->stat('tree_class'));
899
			$parentID = is_numeric($parentID) ? $parentID : 0;
900
			return $list->filter("ParentID", $parentID);
901
		}
902
	}
903
904
	/**
905
	 * @return Form
906
	 */
907
	public function ListViewForm() {
908
		$params = $this->getRequest()->requestVar('q');
909
		$list = $this->getList($params, $parentID = $this->getRequest()->requestVar('ParentID'));
910
		$gridFieldConfig = GridFieldConfig::create()->addComponents(
911
			new GridFieldSortableHeader(),
912
			new GridFieldDataColumns(),
913
			new GridFieldPaginator(self::config()->page_length)
914
		);
915
		if($parentID){
916
			$linkSpec = $this->Link();
917
			$linkSpec = $linkSpec . (strstr($linkSpec, '?') ? '&' : '?') . 'ParentID=%d&view=listview';
918
			$gridFieldConfig->addComponent(
919
				GridFieldLevelup::create($parentID)
920
					->setLinkSpec($linkSpec)
921
					->setAttributes(array('data-pjax' => 'ListViewForm,Breadcrumbs'))
922
			);
923
		}
924
		$gridField = new GridField('Page','Pages', $list, $gridFieldConfig);
925
		/** @var GridFieldDataColumns $columns */
926
		$columns = $gridField->getConfig()->getComponentByType('SilverStripe\\Forms\\GridField\\GridFieldDataColumns');
927
928
		// Don't allow navigating into children nodes on filtered lists
929
		$fields = array(
930
			'getTreeTitle' => _t('SiteTree.PAGETITLE', 'Page Title'),
931
			'singular_name' => _t('SiteTree.PAGETYPE'),
932
			'LastEdited' => _t('SiteTree.LASTUPDATED', 'Last Updated'),
933
		);
934
		/** @var GridFieldSortableHeader $sortableHeader */
935
		$sortableHeader = $gridField->getConfig()->getComponentByType('SilverStripe\\Forms\\GridField\\GridFieldSortableHeader');
936
		$sortableHeader->setFieldSorting(array('getTreeTitle' => 'Title'));
937
		$gridField->getState()->ParentID = $parentID;
938
939
		if(!$params) {
940
			$fields = array_merge(array('listChildrenLink' => ''), $fields);
941
		}
942
943
		$columns->setDisplayFields($fields);
944
		$columns->setFieldCasting(array(
945
			'Created' => 'DBDatetime->Ago',
946
			'LastEdited' => 'DBDatetime->FormatFromSettings',
947
			'getTreeTitle' => 'HTMLFragment'
948
		));
949
950
		$controller = $this;
951
		$columns->setFieldFormatting(array(
952
			'listChildrenLink' => function($value, &$item) use($controller) {
953
				/** @var SiteTree $item */
954
				$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...
955
				if($num) {
956
					return sprintf(
957
						'<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>',
958
						Controller::join_links(
959
							$controller->Link(),
960
							sprintf("?ParentID=%d&view=listview", (int)$item->ID)
961
						),
962
						$num
963
					);
964
				}
965
			},
966
			'getTreeTitle' => function($value, &$item) use($controller) {
967
				return sprintf(
968
					'<a class="action-detail" href="%s">%s</a>',
969
					Controller::join_links(
970
						CMSPageEditController::singleton()->Link('show'),
971
						(int)$item->ID
972
					),
973
					$item->TreeTitle // returns HTML, does its own escaping
974
				);
975
			}
976
		));
977
978
		$negotiator = $this->getResponseNegotiator();
979
		$listview = Form::create(
980
			$this,
981
			'ListViewForm',
982
			new FieldList($gridField),
983
			new FieldList()
984
		)->setHTMLID('Form_ListViewForm');
985
		$listview->setAttribute('data-pjax-fragment', 'ListViewForm');
986 View Code Duplication
		$listview->setValidationResponseCallback(function() use ($negotiator, $listview) {
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...
987
			$request = $this->getRequest();
988
			if($request->isAjax() && $negotiator) {
989
				$listview->setupFormErrors();
990
				$result = $listview->forTemplate();
991
992
				return $negotiator->respond($request, array(
993
					'CurrentForm' => function() use($result) {
994
						return $result;
995
					}
996
				));
997
			}
998
		});
999
1000
		$this->extend('updateListView', $listview);
1001
1002
		$listview->disableSecurityToken();
1003
		return $listview;
1004
	}
1005
1006
	public function currentPageID() {
1007
		$id = parent::currentPageID();
1008
1009
		$this->extend('updateCurrentPageID', $id);
1010
1011
		return $id;
1012
	}
1013
1014
	//------------------------------------------------------------------------------------------//
1015
	// Data saving handlers
1016
1017
	/**
1018
	 * Save and Publish page handler
1019
	 *
1020
	 * @param array $data
1021
	 * @param Form $form
1022
	 * @return HTTPResponse
1023
	 * @throws HTTPResponse_Exception
1024
	 */
1025
	public function save($data, $form) {
1026
		$className = $this->stat('tree_class');
1027
1028
		// Existing or new record?
1029
		$id = $data['ID'];
1030
		if(substr($id,0,3) != 'new') {
1031
			/** @var SiteTree $record */
1032
			$record = DataObject::get_by_id($className, $id);
1033
			// Check edit permissions
1034
			if($record && !$record->canEdit()) {
1035
				return Security::permissionFailure($this);
1036
			}
1037
			if(!$record || !$record->ID) {
1038
				throw new HTTPResponse_Exception("Bad record ID #$id", 404);
1039
			}
1040
		} else {
1041
			if(!$className::singleton()->canCreate()) {
1042
				return Security::permissionFailure($this);
1043
			}
1044
			$record = $this->getNewItem($id, false);
1045
		}
1046
1047
		// Check publishing permissions
1048
		$doPublish = !empty($data['publish']);
1049
		if($record && $doPublish && !$record->canPublish()) {
1050
			return Security::permissionFailure($this);
1051
		}
1052
1053
		// TODO Coupling to SiteTree
1054
		$record->HasBrokenLink = 0;
1055
		$record->HasBrokenFile = 0;
1056
1057
		if (!$record->ObsoleteClassName) {
1058
			$record->writeWithoutVersion();
1059
		}
1060
1061
		// Update the class instance if necessary
1062
		if(isset($data['ClassName']) && $data['ClassName'] != $record->ClassName) {
1063
			// Replace $record with a new instance of the new class
1064
			$newClassName = $data['ClassName'];
1065
			$record = $record->newClassInstance($newClassName);
1066
		}
1067
1068
		// save form data into record
1069
		$form->saveInto($record);
1070
		$record->write();
1071
1072
		// If the 'Save & Publish' button was clicked, also publish the page
1073
		if ($doPublish) {
1074
			$record->publishRecursive();
1075
			$message = _t(
1076
				'CMSMain.PUBLISHED',
1077
				"Published '{title}' successfully.",
1078
				['title' => $record->Title]
0 ignored issues
show
Documentation introduced by
array('title' => $record->Title) is of type array<string,?,{"title":"?"}>, 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...
1079
			);
1080
		} else {
1081
			$message = _t(
1082
				'CMSMain.SAVED',
1083
				"Saved '{title}' successfully.",
1084
				['title' => $record->Title]
0 ignored issues
show
Documentation introduced by
array('title' => $record->Title) is of type array<string,?,{"title":"?"}>, 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...
1085
			);
1086
		}
1087
1088
		$this->getResponse()->addHeader('X-Status', rawurlencode($message));
1089
		return $this->getResponseNegotiator()->respond($this->getRequest());
1090
	}
1091
1092
	/**
1093
	 * @uses LeftAndMainExtension->augmentNewSiteTreeItem()
1094
	 *
1095
	 * @param int|string $id
1096
	 * @param bool $setID
1097
	 * @return mixed|DataObject
1098
	 * @throws HTTPResponse_Exception
1099
	 */
1100
	public function getNewItem($id, $setID = true) {
1101
		$parentClass = $this->stat('tree_class');
1102
		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...
1103
1104
		if (!is_a($className, $parentClass, true)) {
1105
			$response = Security::permissionFailure($this);
1106
			if (!$response) {
1107
				$response = $this->getResponse();
1108
			}
1109
			throw new HTTPResponse_Exception($response);
1110
		}
1111
1112
		/** @var SiteTree $newItem */
1113
		$newItem = Injector::inst()->create($className);
1114
		if( !$suffix ) {
1115
			$sessionTag = "NewItems." . $parentID . "." . $className;
1116
			if(Session::get($sessionTag)) {
1117
				$suffix = '-' . Session::get($sessionTag);
1118
				Session::set($sessionTag, Session::get($sessionTag) + 1);
1119
			}
1120
			else
1121
				Session::set($sessionTag, 1);
1122
1123
				$id = $id . $suffix;
1124
		}
1125
1126
		$newItem->Title = _t(
1127
			'CMSMain.NEWPAGE',
1128
			"New {pagetype}",'followed by a page type title',
1129
			array('pagetype' => singleton($className)->i18n_singular_name())
1130
		);
1131
		$newItem->ClassName = $className;
1132
		$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...
1133
1134
		// DataObject::fieldExists only checks the current class, not the hierarchy
1135
		// This allows the CMS to set the correct sort value
1136
		if($newItem->castingHelper('Sort')) {
1137
			$newItem->Sort = DB::prepared_query('SELECT MAX("Sort") FROM "SiteTree" WHERE "ParentID" = ?', array($parentID))->value() + 1;
1138
		}
1139
1140
		if($setID) $newItem->ID = $id;
1141
1142
		# Some modules like subsites add extra fields that need to be set when the new item is created
1143
		$this->extend('augmentNewSiteTreeItem', $newItem);
1144
1145
		return $newItem;
1146
	}
1147
1148
	/**
1149
	 * Actually perform the publication step
1150
	 *
1151
	 * @param Versioned|DataObject $record
1152
	 * @return mixed
1153
	 */
1154
	public function performPublish($record) {
1155
		if($record && !$record->canPublish()) {
0 ignored issues
show
Bug introduced by
The method canPublish does only exist in SilverStripe\ORM\Versioning\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...
1156
			return Security::permissionFailure($this);
1157
		}
1158
1159
		$record->publishRecursive();
0 ignored issues
show
Bug introduced by
The method publishRecursive does only exist in SilverStripe\ORM\Versioning\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...
1160
	}
1161
1162
	/**
1163
	 * Reverts a page by publishing it to live.
1164
	 * Use {@link restorepage()} if you want to restore a page
1165
	 * which was deleted from draft without publishing.
1166
	 *
1167
	 * @uses SiteTree->doRevertToLive()
1168
	 *
1169
	 * @param array $data
1170
	 * @param Form $form
1171
	 * @return HTTPResponse
1172
	 * @throws HTTPResponse_Exception
1173
	 */
1174
	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...
1175
		if(!isset($data['ID'])) {
1176
			throw new HTTPResponse_Exception("Please pass an ID in the form content", 400);
1177
		}
1178
1179
		$id = (int) $data['ID'];
1180
		$restoredPage = Versioned::get_latest_version(SiteTree::class, $id);
1181
		if(!$restoredPage) {
1182
			throw new HTTPResponse_Exception("SiteTree #$id not found", 400);
1183
		}
1184
1185
		/** @var SiteTree $record */
1186
		$record = Versioned::get_one_by_stage('SilverStripe\\CMS\\Model\\SiteTree', 'Live', array(
0 ignored issues
show
Documentation introduced by
array('"SiteTree_Live"."ID"' => $id) is of type array<string,integer,{"\...e\".\"ID\"":"integer"}>, 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...
1187
			'"SiteTree_Live"."ID"' => $id
1188
		));
1189
1190
		// a user can restore a page without publication rights, as it just adds a new draft state
1191
		// (this action should just be available when page has been "deleted from draft")
1192
		if($record && !$record->canEdit()) {
1193
			return Security::permissionFailure($this);
1194
		}
1195
		if(!$record || !$record->ID) {
1196
			throw new HTTPResponse_Exception("Bad record ID #$id", 404);
1197
		}
1198
1199
		$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...
1200
1201
		$this->getResponse()->addHeader(
1202
			'X-Status',
1203
			rawurlencode(_t(
1204
				'CMSMain.RESTORED',
1205
				"Restored '{title}' successfully",
1206
				'Param %s is a title',
1207
				array('title' => $record->Title)
1208
			))
1209
		);
1210
1211
		return $this->getResponseNegotiator()->respond($this->getRequest());
1212
	}
1213
1214
	/**
1215
	 * Delete the current page from draft stage.
1216
	 *
1217
	 * @see deletefromlive()
1218
	 *
1219
	 * @param array $data
1220
	 * @param Form $form
1221
	 * @return HTTPResponse
1222
	 * @throws HTTPResponse_Exception
1223
	 */
1224 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...
1225
		$id = $data['ID'];
1226
		$record = SiteTree::get()->byID($id);
1227
		if($record && !$record->canDelete()) {
1228
			return Security::permissionFailure();
1229
		}
1230
		if(!$record || !$record->ID) {
1231
			throw new HTTPResponse_Exception("Bad record ID #$id", 404);
1232
		}
1233
1234
		// Delete record
1235
		$record->delete();
1236
1237
		$this->getResponse()->addHeader(
1238
			'X-Status',
1239
			rawurlencode(sprintf(_t('CMSMain.REMOVEDPAGEFROMDRAFT',"Removed '%s' from the draft site"), $record->Title))
1240
		);
1241
1242
		// Even if the record has been deleted from stage and live, it can be viewed in "archive mode"
1243
		return $this->getResponseNegotiator()->respond($this->getRequest());
1244
	}
1245
1246
	/**
1247
	 * Delete this page from both live and stage
1248
	 *
1249
	 * @param array $data
1250
	 * @param Form $form
1251
	 * @return HTTPResponse
1252
	 * @throws HTTPResponse_Exception
1253
	 */
1254 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...
1255
		$id = $data['ID'];
1256
		/** @var SiteTree $record */
1257
		$record = SiteTree::get()->byID($id);
1258
		if(!$record || !$record->exists()) {
1259
			throw new HTTPResponse_Exception("Bad record ID #$id", 404);
1260
		}
1261
		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...
1262
			return Security::permissionFailure();
1263
		}
1264
1265
		// Archive record
1266
		$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...
1267
1268
		$this->getResponse()->addHeader(
1269
			'X-Status',
1270
			rawurlencode(sprintf(_t('CMSMain.ARCHIVEDPAGE',"Archived page '%s'"), $record->Title))
1271
		);
1272
1273
		// Even if the record has been deleted from stage and live, it can be viewed in "archive mode"
1274
		return $this->getResponseNegotiator()->respond($this->getRequest());
1275
	}
1276
1277
	public function publish($data, $form) {
1278
		$data['publish'] = '1';
1279
1280
		return $this->save($data, $form);
1281
	}
1282
1283
	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...
1284
		$className = $this->stat('tree_class');
1285
		/** @var SiteTree $record */
1286
		$record = DataObject::get_by_id($className, $data['ID']);
1287
1288
		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...
1289
			return Security::permissionFailure($this);
1290
		}
1291
		if(!$record || !$record->ID) {
1292
			throw new HTTPResponse_Exception("Bad record ID #" . (int)$data['ID'], 404);
1293
		}
1294
1295
		$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...
1296
1297
		$this->getResponse()->addHeader(
1298
			'X-Status',
1299
			rawurlencode(_t('CMSMain.REMOVEDPAGE',"Removed '{title}' from the published site", array('title' => $record->Title)))
0 ignored issues
show
Documentation introduced by
array('title' => $record->Title) is of type array<string,string,{"title":"string"}>, 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...
1300
		);
1301
1302
		return $this->getResponseNegotiator()->respond($this->getRequest());
1303
	}
1304
1305
	/**
1306
	 * @return HTTPResponse
1307
	 */
1308
	public function rollback() {
1309
		return $this->doRollback(array(
1310
			'ID' => $this->currentPageID(),
1311
			'Version' => $this->getRequest()->param('VersionID')
1312
		), 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...
1313
	}
1314
1315
	/**
1316
	 * Rolls a site back to a given version ID
1317
	 *
1318
	 * @param array $data
1319
	 * @param Form $form
1320
	 * @return HTTPResponse
1321
	 */
1322
	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...
1323
		$this->extend('onBeforeRollback', $data['ID']);
1324
1325
		$id = (isset($data['ID'])) ? (int) $data['ID'] : null;
1326
		$version = (isset($data['Version'])) ? (int) $data['Version'] : null;
1327
1328
		/** @var DataObject|Versioned $record */
1329
		$record = DataObject::get_by_id($this->stat('tree_class'), $id);
1330
		if($record && !$record->canEdit()) {
0 ignored issues
show
Bug introduced by
The call to canEdit() misses a required argument $member.

This check looks for function calls that miss required arguments.

Loading history...
1331
			return Security::permissionFailure($this);
1332
		}
1333
1334
		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...
1335
			$record->doRollbackTo($version);
0 ignored issues
show
Bug introduced by
The method doRollbackTo does only exist in SilverStripe\ORM\Versioning\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...
1336
			$message = _t(
1337
				'CMSMain.ROLLEDBACKVERSIONv2',
1338
				"Rolled back to version #%d.",
1339
				array('version' => $data['Version'])
0 ignored issues
show
Documentation introduced by
array('version' => $data['Version']) is of type array<string,?,{"version":"?"}>, 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...
1340
			);
1341
		} else {
1342
			$record->doRevertToLive();
0 ignored issues
show
Bug introduced by
The method doRevertToLive does only exist in SilverStripe\ORM\Versioning\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...
1343
			$message = _t(
1344
				'CMSMain.ROLLEDBACKPUBv2',"Rolled back to published version."
1345
			);
1346
		}
1347
1348
		$this->getResponse()->addHeader('X-Status', rawurlencode($message));
1349
1350
		// Can be used in different contexts: In normal page edit view, in which case the redirect won't have any effect.
1351
		// Or in history view, in which case a revert causes the CMS to re-load the edit view.
1352
		// The X-Pjax header forces a "full" content refresh on redirect.
1353
		$url = Controller::join_links(CMSPageEditController::singleton()->Link('show'), $record->ID);
1354
		$this->getResponse()->addHeader('X-ControllerURL', $url);
1355
		$this->getRequest()->addHeader('X-Pjax', 'Content');
1356
		$this->getResponse()->addHeader('X-Pjax', 'Content');
1357
1358
		return $this->getResponseNegotiator()->respond($this->getRequest());
1359
	}
1360
1361
	/**
1362
	 * Batch Actions Handler
1363
	 */
1364
	public function batchactions() {
1365
		return new CMSBatchActionHandler($this, 'batchactions');
1366
	}
1367
1368
	public function BatchActionParameters() {
1369
		$batchActions = CMSBatchActionHandler::config()->batch_actions;
1370
1371
		$forms = array();
1372
		foreach($batchActions as $urlSegment => $batchAction) {
1373
			$SNG_action = singleton($batchAction);
1374
			if ($SNG_action->canView() && $fieldset = $SNG_action->getParameterFields()) {
1375
				$formHtml = '';
1376
				/** @var FormField $field */
1377
				foreach($fieldset as $field) {
1378
					$formHtml .= $field->Field();
1379
				}
1380
				$forms[$urlSegment] = $formHtml;
1381
			}
1382
		}
1383
		$pageHtml = '';
1384
		foreach($forms as $urlSegment => $html) {
1385
			$pageHtml .= "<div class=\"params\" id=\"BatchActionParameters_$urlSegment\">$html</div>\n\n";
1386
		}
1387
		return new LiteralField("BatchActionParameters", '<div id="BatchActionParameters" style="display:none">'.$pageHtml.'</div>');
1388
	}
1389
	/**
1390
	 * Returns a list of batch actions
1391
	 */
1392
	public function BatchActionList() {
1393
		return $this->batchactions()->batchActionList();
1394
	}
1395
1396
	public function publishall($request) {
1397
		if(!Permission::check('ADMIN')) return Security::permissionFailure($this);
1398
1399
		increase_time_limit_to();
1400
		increase_memory_limit_to();
1401
1402
		$response = "";
1403
1404
		if(isset($this->requestParams['confirm'])) {
1405
			// Protect against CSRF on destructive action
1406
			if(!SecurityToken::inst()->checkRequest($request)) return $this->httpError(400);
1407
1408
			$start = 0;
1409
			$pages = SiteTree::get()->limit("$start,30");
1410
			$count = 0;
1411
			while($pages) {
1412
				/** @var SiteTree $page */
1413
				foreach($pages as $page) {
1414
					if($page && !$page->canPublish()) {
1415
						return Security::permissionFailure($this);
1416
					}
1417
1418
					$page->publishRecursive();
1419
					$page->destroy();
1420
					unset($page);
1421
					$count++;
1422
					$response .= "<li>$count</li>";
1423
				}
1424
				if($pages->count() > 29) {
1425
					$start += 30;
1426
					$pages = SiteTree::get()->limit("$start,30");
1427
				} else {
1428
					break;
1429
				}
1430
			}
1431
			$response .= _t('CMSMain.PUBPAGES',"Done: Published {count} pages", array('count' => $count));
0 ignored issues
show
Documentation introduced by
array('count' => $count) is of type array<string,integer,{"count":"integer"}>, 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...
1432
1433
		} else {
1434
			$token = SecurityToken::inst();
1435
			$fields = new FieldList();
1436
			$token->updateFieldSet($fields);
1437
			$tokenField = $fields->first();
1438
			$tokenHtml = ($tokenField) ? $tokenField->FieldHolder() : '';
1439
			$response .= '<h1>' . _t('CMSMain.PUBALLFUN','"Publish All" functionality') . '</h1>
1440
				<p>' . _t('CMSMain.PUBALLFUN2', 'Pressing this button will do the equivalent of going to every page and pressing "publish".  It\'s
1441
				intended to be used after there have been massive edits of the content, such as when the site was
1442
				first built.') . '</p>
1443
				<form method="post" action="publishall">
1444
					<input type="submit" name="confirm" value="'
1445
					. _t('CMSMain.PUBALLCONFIRM',"Please publish every page in the site, copying content stage to live",'Confirmation button') .'" />'
1446
					. $tokenHtml .
1447
				'</form>';
1448
		}
1449
1450
		return $response;
1451
	}
1452
1453
	/**
1454
	 * Restore a completely deleted page from the SiteTree_versions table.
1455
	 *
1456
	 * @param array $data
1457
	 * @param Form $form
1458
	 * @return HTTPResponse
1459
	 */
1460
	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...
1461
		if(!isset($data['ID']) || !is_numeric($data['ID'])) {
1462
			return new HTTPResponse("Please pass an ID in the form content", 400);
1463
		}
1464
1465
		$id = (int)$data['ID'];
1466
		/** @var SiteTree $restoredPage */
1467
		$restoredPage = Versioned::get_latest_version(SiteTree::class, $id);
1468
		if(!$restoredPage) {
1469
			return new HTTPResponse("SiteTree #$id not found", 400);
1470
		}
1471
1472
		$restoredPage = $restoredPage->doRestoreToStage();
1473
1474
		$this->getResponse()->addHeader(
1475
			'X-Status',
1476
			rawurlencode(_t(
1477
				'CMSMain.RESTORED',
1478
				"Restored '{title}' successfully",
1479
				array('title' => $restoredPage->Title)
0 ignored issues
show
Documentation introduced by
array('title' => $restoredPage->Title) is of type array<string,string,{"title":"string"}>, 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...
1480
			))
1481
		);
1482
1483
		return $this->getResponseNegotiator()->respond($this->getRequest());
1484
	}
1485
1486
	public function duplicate($request) {
1487
		// Protect against CSRF on destructive action
1488
		if(!SecurityToken::inst()->checkRequest($request)) return $this->httpError(400);
1489
1490
		if(($id = $this->urlParams['ID']) && is_numeric($id)) {
1491
			/** @var SiteTree $page */
1492
			$page = SiteTree::get()->byID($id);
1493 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...
1494
				return Security::permissionFailure($this);
1495
			}
1496
			if(!$page || !$page->ID) throw new HTTPResponse_Exception("Bad record ID #$id", 404);
1497
1498
			$newPage = $page->duplicate();
1499
1500
			// ParentID can be hard-set in the URL.  This is useful for pages with multiple parents
1501
			if(isset($_GET['parentID']) && is_numeric($_GET['parentID'])) {
1502
				$newPage->ParentID = $_GET['parentID'];
1503
				$newPage->write();
1504
			}
1505
1506
			$this->getResponse()->addHeader(
1507
				'X-Status',
1508
				rawurlencode(_t(
1509
					'CMSMain.DUPLICATED',
1510
					"Duplicated '{title}' successfully",
1511
					array('title' => $newPage->Title)
0 ignored issues
show
Documentation introduced by
array('title' => $newPage->Title) is of type array<string,?,{"title":"?"}>, 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...
1512
				))
1513
			);
1514
			$url = Controller::join_links(CMSPageEditController::singleton()->Link('show'), $newPage->ID);
1515
			$this->getResponse()->addHeader('X-ControllerURL', $url);
1516
			$this->getRequest()->addHeader('X-Pjax', 'Content');
1517
			$this->getResponse()->addHeader('X-Pjax', 'Content');
1518
1519
			return $this->getResponseNegotiator()->respond($this->getRequest());
1520
		} else {
1521
			return new HTTPResponse("CMSMain::duplicate() Bad ID: '$id'", 400);
1522
		}
1523
	}
1524
1525
	public function duplicatewithchildren($request) {
1526
		// Protect against CSRF on destructive action
1527
		if(!SecurityToken::inst()->checkRequest($request)) return $this->httpError(400);
1528
		increase_time_limit_to();
1529
		if(($id = $this->urlParams['ID']) && is_numeric($id)) {
1530
			/** @var SiteTree $page */
1531
			$page = SiteTree::get()->byID($id);
1532 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...
1533
				return Security::permissionFailure($this);
1534
			}
1535
			if(!$page || !$page->ID) throw new HTTPResponse_Exception("Bad record ID #$id", 404);
1536
1537
			$newPage = $page->duplicateWithChildren();
1538
1539
			$this->getResponse()->addHeader(
1540
				'X-Status',
1541
				rawurlencode(_t(
1542
					'CMSMain.DUPLICATEDWITHCHILDREN',
1543
					"Duplicated '{title}' and children successfully",
1544
					array('title' => $newPage->Title)
0 ignored issues
show
Documentation introduced by
array('title' => $newPage->Title) is of type array<string,string,{"title":"string"}>, 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...
1545
				))
1546
			);
1547
			$url = Controller::join_links(CMSPageEditController::singleton()->Link('show'), $newPage->ID);
1548
			$this->getResponse()->addHeader('X-ControllerURL', $url);
1549
			$this->getRequest()->addHeader('X-Pjax', 'Content');
1550
			$this->getResponse()->addHeader('X-Pjax', 'Content');
1551
1552
			return $this->getResponseNegotiator()->respond($this->getRequest());
1553
		} else {
1554
			return new HTTPResponse("CMSMain::duplicatewithchildren() Bad ID: '$id'", 400);
1555
		}
1556
	}
1557
1558
	public function providePermissions() {
1559
		$title = CMSPagesController::menu_title();
1560
		return array(
1561
			"CMS_ACCESS_CMSMain" => array(
1562
				'name' => _t('CMSMain.ACCESS', "Access to '{title}' section", array('title' => $title)),
0 ignored issues
show
Documentation introduced by
array('title' => $title) is of type array<string,string,{"title":"string"}>, 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...
1563
				'category' => _t('Permission.CMS_ACCESS_CATEGORY', 'CMS Access'),
1564
				'help' => _t(
1565
					'CMSMain.ACCESS_HELP',
1566
					'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".'
1567
				),
1568
				'sort' => -99 // below "CMS_ACCESS_LeftAndMain", but above everything else
1569
			)
1570
		);
1571
	}
1572
1573
}
1574