Completed
Push — master ( 36de85...554bbc )
by Daniel
10s
created

CMSMain::save()   C

Complexity

Conditions 14
Paths 21

Size

Total Lines 66
Code Lines 38

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 66
rs 5.8815
c 0
b 0
f 0
cc 14
eloc 38
nc 21
nop 2

How to fix   Long Method    Complexity   

Long Method

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

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

Commonly applied refactorings include:

1
<?php
2
3
namespace SilverStripe\CMS\Controllers;
4
5
use SilverStripe\Admin\AdminRootController;
6
use SilverStripe\Admin\CMSBatchActionHandler;
7
use SilverStripe\Admin\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\ValidationResult;
53
use SilverStripe\ORM\Versioning\Versioned;
54
use SilverStripe\Security\Member;
55
use SilverStripe\Security\Permission;
56
use SilverStripe\Security\PermissionProvider;
57
use SilverStripe\Security\Security;
58
use SilverStripe\Security\SecurityToken;
59
use SilverStripe\View\ArrayData;
60
use SilverStripe\View\Requirements;
61
use Translatable;
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_icon_class = 'font-icon-sitemap';
89
90
	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...
91
92
	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...
93
94
	private static $subitem_class = Member::class;
95
96
	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...
97
98
	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...
99
100
	/**
101
	 * Amount of results showing on a single page.
102
	 *
103
	 * @config
104
	 * @var int
105
	 */
106
	private static $page_length = 15;
107
108
	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...
109
		'archive',
110
		'deleteitems',
111
		'DeleteItemsForm',
112
		'dialog',
113
		'duplicate',
114
		'duplicatewithchildren',
115
		'publishall',
116
		'publishitems',
117
		'PublishItemsForm',
118
		'submit',
119
		'EditForm',
120
		'SearchForm',
121
		'SiteTreeAsUL',
122
		'getshowdeletedsubtree',
123
		'batchactions',
124
		'treeview',
125
		'listview',
126
		'ListViewForm',
127
		'childfilter',
128
	);
129
130
	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...
131
		'TreeIsFiltered' => 'Boolean',
132
		'AddForm' => 'HTMLFragment',
133
		'LinkPages' => 'Text',
134
		'Link' => 'Text',
135
		'ListViewForm' => 'HTMLFragment',
136
		'ExtraTreeTools' => 'HTMLFragment',
137
		'PageList' => 'HTMLFragment',
138
		'PageListSidebar' => 'HTMLFragment',
139
		'SiteTreeHints' => 'HTMLFragment',
140
		'SecurityID' => 'Text',
141
		'SiteTreeAsUL' => 'HTMLFragment',
142
	);
143
144
	protected function init() {
145
		// set reading lang
146
		if(SiteTree::has_extension('Translatable') && !$this->getRequest()->isAjax()) {
147
			Translatable::choose_site_locale(array_keys(Translatable::get_existing_content_languages('SilverStripe\\CMS\\Model\\SiteTree')));
148
		}
149
150
		parent::init();
151
152
		Requirements::javascript(CMS_DIR . '/client/dist/js/bundle.js');
153
		Requirements::javascript(CMS_DIR . '/client/dist/js/SilverStripeNavigator.js');
154
		Requirements::css(CMS_DIR . '/client/dist/styles/bundle.css');
155
		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...
156
		Requirements::add_i18n_javascript(CMS_DIR . '/client/lang', false, true);
157
158
		CMSBatchActionHandler::register('restore', CMSBatchAction_Restore::class);
159
		CMSBatchActionHandler::register('archive', CMSBatchAction_Archive::class);
160
		CMSBatchActionHandler::register('unpublish', CMSBatchAction_Unpublish::class);
161
		CMSBatchActionHandler::register('publish', CMSBatchAction_Publish::class);
162
	}
163
164
	public function index($request) {
165
		// In case we're not showing a specific record, explicitly remove any session state,
166
		// to avoid it being highlighted in the tree, and causing an edit form to show.
167
		if(!$request->param('Action')) {
168
			$this->setCurrentPageID(null);
169
		}
170
171
		return parent::index($request);
172
	}
173
174
	public function getResponseNegotiator() {
175
		$negotiator = parent::getResponseNegotiator();
176
177
		// ListViewForm
178
		$negotiator->setCallback('ListViewForm', function()  {
179
			return $this->ListViewForm()->forTemplate();
180
		});
181
182
		// PageList view
183
		$negotiator->setCallback('Content-PageList', function () {
184
			return $this->PageList()->forTemplate();
185
		});
186
187
		// PageList view for edit controller
188
		$negotiator->setCallback('Content-PageList-Sidebar', function() {
189
			return $this->PageListSidebar()->forTemplate();
190
		});
191
192
		return $negotiator;
193
	}
194
195
	/**
196
	 * Get pages listing area
197
	 *
198
	 * @return DBHTMLText
199
	 */
200
	public function PageList() {
201
		return $this->renderWith($this->getTemplatesWithSuffix('_PageList'));
202
	}
203
204
	/**
205
	 * Page list view for edit-form
206
	 *
207
	 * @return DBHTMLText
208
	 */
209
	public function PageListSidebar() {
210
		return $this->renderWith($this->getTemplatesWithSuffix('_PageList_Sidebar'));
211
	}
212
213
	/**
214
	 * If this is set to true, the "switchView" context in the
215
	 * template is shown, with links to the staging and publish site.
216
	 *
217
	 * @return boolean
218
	 */
219
	public function ShowSwitchView() {
220
		return true;
221
	}
222
223
	/**
224
	 * Overloads the LeftAndMain::ShowView. Allows to pass a page as a parameter, so we are able
225
	 * to switch view also for archived versions.
226
	 *
227
	 * @param SiteTree $page
228
	 * @return array
229
	 */
230
	public function SwitchView($page = null) {
231
		if(!$page) {
232
			$page = $this->currentPage();
233
		}
234
235
		if($page) {
236
			$nav = SilverStripeNavigator::get_for_record($page);
237
			return $nav['items'];
238
		}
239
	}
240
241
	//------------------------------------------------------------------------------------------//
242
	// Main controllers
243
244
	//------------------------------------------------------------------------------------------//
245
	// Main UI components
246
247
	/**
248
	 * Override {@link LeftAndMain} Link to allow blank URL segment for CMSMain.
249
	 *
250
	 * @param string|null $action Action to link to.
251
	 * @return string
252
	 */
253
	public function Link($action = null) {
254
		$link = Controller::join_links(
255
			AdminRootController::admin_url(),
256
			$this->stat('url_segment'), // in case we want to change the segment
257
			'/', // trailing slash needed if $action is null!
258
			"$action"
259
		);
260
		$this->extend('updateLink', $link);
261
		return $link;
262
	}
263
264
	public function LinkPages() {
265
		return CMSPagesController::singleton()->Link();
266
	}
267
268
	public function LinkPagesWithSearch() {
269
		return $this->LinkWithSearch($this->LinkPages());
270
	}
271
272
	/**
273
	 * Get link to tree view
274
	 *
275
	 * @return string
276
	 */
277
	public function LinkTreeView() {
278
		// Tree view is just default link to main pages section (no /treeview suffix)
279
		return $this->LinkWithSearch(CMSMain::singleton()->Link());
280
	}
281
282
	/**
283
	 * Get link to list view
284
	 *
285
	 * @return string
286
	 */
287
	public function LinkListView() {
288
		// Note : Force redirect to top level page controller
289
		return $this->LinkWithSearch(CMSMain::singleton()->Link('listview'));
290
	}
291
292 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...
293
		if(!$id) {
294
			$id = $this->currentPageID();
295
		}
296
		return $this->LinkWithSearch(
297
			Controller::join_links(CMSPageEditController::singleton()->Link('show'), $id)
298
		);
299
	}
300
301 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...
302
		if($id = $this->currentPageID()) {
303
			return $this->LinkWithSearch(
304
				Controller::join_links(CMSPageSettingsController::singleton()->Link('show'), $id)
305
			);
306
		} else {
307
			return null;
308
		}
309
	}
310
311 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...
312
		if($id = $this->currentPageID()) {
313
			return $this->LinkWithSearch(
314
				Controller::join_links(CMSPageHistoryController::singleton()->Link('show'), $id)
315
			);
316
		} else {
317
			return null;
318
		}
319
	}
320
321
	public function LinkWithSearch($link) {
322
		// Whitelist to avoid side effects
323
		$params = array(
324
			'q' => (array)$this->getRequest()->getVar('q'),
325
			'ParentID' => $this->getRequest()->getVar('ParentID')
326
		);
327
		$link = Controller::join_links(
328
			$link,
329
			array_filter(array_values($params)) ? '?' . http_build_query($params) : null
330
		);
331
		$this->extend('updateLinkWithSearch', $link);
332
		return $link;
333
	}
334
335
	public function LinkPageAdd($extra = null, $placeholders = null) {
336
		$link = CMSPageAddController::singleton()->Link();
337
		$this->extend('updateLinkPageAdd', $link);
338
339
		if($extra) {
340
			$link = Controller::join_links ($link, $extra);
341
		}
342
343
		if($placeholders) {
344
			$link .= (strpos($link, '?') === false ? "?$placeholders" : "&$placeholders");
345
		}
346
347
		return $link;
348
	}
349
350
	/**
351
	 * @return string
352
	 */
353
	public function LinkPreview() {
354
		$record = $this->getRecord($this->currentPageID());
355
		$baseLink = Director::absoluteBaseURL();
356
		if ($record && $record instanceof SiteTree) {
357
			// if we are an external redirector don't show a link
358
			if ($record instanceof RedirectorPage && $record->RedirectionType == 'External') {
359
				$baseLink = false;
360
			}
361
			else {
362
				$baseLink = $record->Link('?stage=Stage');
363
			}
364
		}
365
		return $baseLink;
366
	}
367
368
	/**
369
	 * Return the entire site tree as a nested set of ULs
370
	 */
371
	public function SiteTreeAsUL() {
372
		// Pre-cache sitetree version numbers for querying efficiency
373
		Versioned::prepopulate_versionnumber_cache(SiteTree::class, "Stage");
374
		Versioned::prepopulate_versionnumber_cache(SiteTree::class, "Live");
375
		$html = $this->getSiteTreeFor($this->stat('tree_class'));
376
377
		$this->extend('updateSiteTreeAsUL', $html);
378
379
		return $html;
380
	}
381
382
	/**
383
	 * @return boolean
384
	 */
385
	public function TreeIsFiltered() {
386
		$query = $this->getRequest()->getVar('q');
387
388
		if (!$query || (count($query) === 1 && isset($query['FilterClass']) && $query['FilterClass'] === 'SilverStripe\\CMS\\Controllers\\CMSSiteTreeFilter_Search')) {
389
			return false;
390
		}
391
392
		return true;
393
	}
394
395
	public function ExtraTreeTools() {
396
		$html = '';
397
		$this->extend('updateExtraTreeTools', $html);
398
		return $html;
399
	}
400
401
	/**
402
	 * Returns a Form for page searching for use in templates.
403
	 *
404
	 * Can be modified from a decorator by a 'updateSearchForm' method
405
	 *
406
	 * @return Form
407
	 */
408
	public function SearchForm() {
409
		// Create the fields
410
		$content = new TextField('q[Term]', _t('CMSSearch.FILTERLABELTEXT', 'Search'));
411
		$dateFrom = new DateField(
412
			'q[LastEditedFrom]',
413
			_t('CMSSearch.FILTERDATEFROM', 'From')
414
		);
415
		$dateFrom->setConfig('showcalendar', true);
416
		$dateTo = new DateField(
417
			'q[LastEditedTo]',
418
			_t('CMSSearch.FILTERDATETO', 'To')
419
		);
420
		$dateTo->setConfig('showcalendar', true);
421
		$pageFilter = new DropdownField(
422
			'q[FilterClass]',
423
			_t('CMSMain.PAGES', 'Page status'),
424
			CMSSiteTreeFilter::get_all_filters()
425
		);
426
		$pageClasses = new DropdownField(
427
			'q[ClassName]',
428
			_t('CMSMain.PAGETYPEOPT', 'Page type', 'Dropdown for limiting search to a page type'),
429
			$this->getPageTypes()
430
		);
431
		$pageClasses->setEmptyString(_t('CMSMain.PAGETYPEANYOPT','Any'));
432
433
		// Group the Datefields
434
		$dateGroup = new FieldGroup(
435
			$dateFrom,
436
			$dateTo
437
		);
438
		$dateGroup->setTitle(_t('CMSSearch.PAGEFILTERDATEHEADING', 'Last edited'));
439
440
		// view mode
441
		$viewMode = HiddenField::create('view', false, $this->ViewState());
442
443
		// Create the Field list
444
		$fields = new FieldList(
445
			$content,
446
			$pageFilter,
447
			$pageClasses,
448
			$dateGroup,
449
			$viewMode
450
		);
451
452
		// Create the Search and Reset action
453
		$actions = new FieldList(
454
			FormAction::create('doSearch',  _t('CMSMain_left_ss.APPLY_FILTER', 'Search'))
455
				->addExtraClass('btn btn-primary'),
456
			ResetFormAction::create('clear', _t('CMSMain_left_ss.CLEAR_FILTER', 'Clear'))
457
				->addExtraClass('btn btn-secondary')
458
		);
459
460
		// Use <button> to allow full jQuery UI styling on the all of the Actions
461
		/** @var FormAction $action */
462
		foreach($actions->dataFields() as $action) {
463
			/** @var FormAction $action */
464
			$action->setUseButtonTag(true);
465
		}
466
467
		// Create the form
468
		/** @skipUpgrade */
469
		$form = Form::create($this, 'SearchForm', $fields, $actions)
470
			->addExtraClass('cms-search-form')
471
			->setFormMethod('GET')
472
			->setFormAction($this->Link())
473
			->disableSecurityToken()
474
			->unsetValidator();
475
476
		// Load the form with previously sent search data
477
		$form->loadDataFrom($this->getRequest()->getVars());
478
479
		// Allow decorators to modify the form
480
		$this->extend('updateSearchForm', $form);
481
482
		return $form;
483
	}
484
485
	/**
486
	 * Returns a sorted array suitable for a dropdown with pagetypes and their translated name
487
	 *
488
	 * @return array
489
	 */
490
	protected function getPageTypes() {
491
		$pageTypes = array();
492
		foreach(SiteTree::page_type_classes() as $pageTypeClass) {
493
			$pageTypes[$pageTypeClass] = SiteTree::singleton($pageTypeClass)->i18n_singular_name();
494
		}
495
		asort($pageTypes);
496
		return $pageTypes;
497
	}
498
499
	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...
500
		return $this->getsubtree($this->getRequest());
501
	}
502
503
	/**
504
	 * @param bool $unlinked
505
	 * @return ArrayList
506
	 */
507
	public function Breadcrumbs($unlinked = false) {
508
		$items = parent::Breadcrumbs($unlinked);
509
510
		if($items->count() > 1) {
511
			// Specific to the SiteTree admin section, we never show the cms section and current
512
			// page in the same breadcrumbs block.
513
			$items->shift();
514
		}
515
516
		return $items;
517
	}
518
519
	/**
520
	 * Create serialized JSON string with site tree hints data to be injected into
521
	 * 'data-hints' attribute of root node of jsTree.
522
	 *
523
	 * @return string Serialized JSON
524
	 */
525
	public function SiteTreeHints() {
526
		$classes = SiteTree::page_type_classes();
527
528
	 	$cacheCanCreate = array();
529
	 	foreach($classes as $class) $cacheCanCreate[$class] = singleton($class)->canCreate();
530
531
	 	// Generate basic cache key. Too complex to encompass all variations
532
	 	$cache = Cache::factory('CMSMain_SiteTreeHints');
533
	 	$cacheKey = md5(implode('_', array(Member::currentUserID(), implode(',', $cacheCanCreate), implode(',', $classes))));
534
	 	if($this->getRequest()->getVar('flush')) {
535
	 		$cache->clean(Zend_Cache::CLEANING_MODE_ALL);
536
		}
537
	 	$json = $cache->load($cacheKey);
538
	 	if(!$json) {
539
			$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...
540
			$def['Root']['disallowedChildren'] = array();
541
542
			// Contains all possible classes to support UI controls listing them all,
543
			// such as the "add page here" context menu.
544
			$def['All'] = array();
545
546
			// Identify disallows and set globals
547
			foreach($classes as $class) {
548
				$obj = singleton($class);
549
				if($obj instanceof HiddenClass) continue;
550
551
				// Name item
552
				$def['All'][$class] = array(
553
					'title' => $obj->i18n_singular_name()
554
				);
555
556
				// Check if can be created at the root
557
				$needsPerm = $obj->stat('need_permission');
558
				if(
559
					!$obj->stat('can_be_root')
560
					|| (!array_key_exists($class, $cacheCanCreate) || !$cacheCanCreate[$class])
561
					|| ($needsPerm && !$this->can($needsPerm))
562
				) {
563
					$def['Root']['disallowedChildren'][] = $class;
564
				}
565
566
				// Hint data specific to the class
567
				$def[$class] = array();
568
569
				$defaultChild = $obj->defaultChild();
570
				if($defaultChild !== 'Page' && $defaultChild !== null) {
571
					$def[$class]['defaultChild'] = $defaultChild;
572
				}
573
574
				$defaultParent = $obj->defaultParent();
575
				if ($defaultParent !== 1 && $defaultParent !== null) {
576
					$def[$class]['defaultParent'] = $defaultParent;
577
				}
578
			}
579
580
			$this->extend('updateSiteTreeHints', $def);
581
582
			$json = Convert::raw2json($def);
583
			$cache->save($json, $cacheKey);
584
		}
585
		return $json;
586
	}
587
588
	/**
589
	 * Populates an array of classes in the CMS
590
	 * which allows the user to change the page type.
591
	 *
592
	 * @return SS_List
593
	 */
594
	public function PageTypes() {
595
		$classes = SiteTree::page_type_classes();
596
597
		$result = new ArrayList();
598
599
		foreach($classes as $class) {
600
            $instance = SiteTree::singleton($class);
601
			if($instance instanceof HiddenClass) {
602
				continue;
603
			}
604
605
			// skip this type if it is restricted
606
			if($instance->stat('need_permission') && !$this->can(singleton($class)->stat('need_permission'))) {
607
				continue;
608
			}
609
610
			$singularName = $instance->i18n_singular_name();
611
			$description = $instance->i18n_description();
612
613
			$result->push(new ArrayData(array(
614
				'ClassName' => $class,
615
				'AddAction' => $singularName,
616
				'Description' => $description,
617
				// TODO Sprite support
618
				'IconURL' => $instance->stat('icon'),
619
				'Title' => $singularName,
620
			)));
621
		}
622
623
		$result = $result->sort('AddAction');
624
625
		return $result;
626
	}
627
628
	/**
629
	 * Get a database record to be managed by the CMS.
630
	 *
631
	 * @param int $id Record ID
632
	 * @param int $versionID optional Version id of the given record
633
	 * @return SiteTree
634
	 */
635
 	public function getRecord($id, $versionID = null) {
636
		$treeClass = $this->stat('tree_class');
637
638
		if($id instanceof $treeClass) {
639
			return $id;
640
		}
641
		else if($id && is_numeric($id)) {
642
			$currentStage = Versioned::get_reading_mode();
643
644
			if($this->getRequest()->getVar('Version')) {
645
				$versionID = (int) $this->getRequest()->getVar('Version');
646
			}
647
648
			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...
649
				$record = Versioned::get_version($treeClass, $id, $versionID);
650
			} else {
651
				$record = DataObject::get_by_id($treeClass, $id);
652
			}
653
654
			// Then, try getting a record from the live site
655
			if(!$record) {
656
				// $record = Versioned::get_one_by_stage($treeClass, "Live", "\"$treeClass\".\"ID\" = $id");
657
				Versioned::set_stage(Versioned::LIVE);
658
				singleton($treeClass)->flushCache();
659
660
				$record = DataObject::get_by_id($treeClass, $id);
661
			}
662
663
			// Then, try getting a deleted record
664
			if(!$record) {
665
				$record = Versioned::get_latest_version($treeClass, $id);
666
			}
667
668
			// Don't open a page from a different locale
669
			/** The record's Locale is saved in database in 2.4, and not related with Session,
670
			 *  we should not check their locale matches the Translatable::get_current_locale,
671
			 * 	here as long as we all the HTTPRequest is init with right locale.
672
			 *	This bit breaks the all FileIFrameField functions if the field is used in CMS
673
			 *  and its relevent ajax calles, like loading the tree dropdown for TreeSelectorField.
674
			 */
675
			/* if($record && SiteTree::has_extension('Translatable') && $record->Locale && $record->Locale != Translatable::get_current_locale()) {
676
				$record = null;
677
			}*/
678
679
			// Set the reading mode back to what it was.
680
			Versioned::set_reading_mode($currentStage);
681
682
			return $record;
683
684
		} else if(substr($id,0,3) == 'new') {
685
			return $this->getNewItem($id);
686
		}
687
	}
688
689
	/**
690
	 * @param int $id
691
	 * @param FieldList $fields
692
	 * @return Form
693
	 */
694
	public function getEditForm($id = null, $fields = null) {
695
		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...
696
		$form = parent::getEditForm($id, $fields);
697
698
		// TODO Duplicate record fetching (see parent implementation)
699
		$record = $this->getRecord($id);
700
		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 700 which is incompatible with the return type documented by SilverStripe\CMS\Controllers\CMSMain::getEditForm of type SilverStripe\Forms\Form.
Loading history...
701
702
		if(!$fields) $fields = $form->Fields();
703
		$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...
704
705
		if($record) {
706
			$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...
707
708
			$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...
709
			// Necessary for different subsites
710
			$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...
711
			$fields->push($liveLinkField = new HiddenField("LiveLink"));
712
			$fields->push($stageLinkField = new HiddenField("StageLink"));
713
			$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...
714
715
			if($record->ID && is_numeric( $record->ID ) ) {
716
				$liveLink = $record->getAbsoluteLiveLink();
717
				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...
718
				if(!$deletedFromStage) {
719
					$stageLink = Controller::join_links($record->AbsoluteLink(), '?stage=Stage');
720
					if($stageLink) $stageLinkField->setValue($stageLink);
721
				}
722
			}
723
724
			// Added in-line to the form, but plucked into different view by LeftAndMain.Preview.js upon load
725
			/** @skipUpgrade */
726
			if($record instanceof CMSPreviewable && !$fields->fieldByName('SilverStripeNavigator')) {
727
				$navField = new LiteralField('SilverStripeNavigator', $this->getSilverStripeNavigator());
728
				$navField->setAllowHTML(true);
729
				$fields->push($navField);
730
			}
731
732
			// getAllCMSActions can be used to completely redefine the action list
733
			if($record->hasMethod('getAllCMSActions')) {
734
				$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...
735
			} else {
736
				$actions = $record->getCMSActions();
737
738
				// Find and remove action menus that have no actions.
739
				if ($actions && $actions->count()) {
740
					/** @var TabSet $tabset */
741
					$tabset = $actions->fieldByName('ActionMenus');
742
					if ($tabset) {
743
						foreach ($tabset->getChildren() as $tab) {
744
							if (!$tab->getChildren()->count()) {
745
								$tabset->removeByName($tab->getName());
746
							}
747
						}
748
					}
749
				}
750
			}
751
752
			// Use <button> to allow full jQuery UI styling
753
			$actionsFlattened = $actions->dataFields();
754
			if($actionsFlattened) {
755
				/** @var FormAction $action */
756
				foreach($actionsFlattened as $action) {
757
					$action->setUseButtonTag(true);
758
				}
759
			}
760
761
			if($record->hasMethod('getCMSValidator')) {
762
				$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...
763
			} else {
764
				$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...
765
			}
766
767
			// TODO Can't merge $FormAttributes in template at the moment
768
			$form->addExtraClass('center ' . $this->BaseCSSClasses());
769
			// Set validation exemptions for specific actions
770
			$form->setValidationExemptActions(array('restore', 'revert', 'deletefromlive', 'delete', 'unpublish', 'rollback', 'doRollback'));
771
772
			// Announce the capability so the frontend can decide whether to allow preview or not.
773
			if ($record instanceof CMSPreviewable) {
774
				$form->addExtraClass('cms-previewable');
775
			}
776
			$form->addExtraClass('fill-height flexbox-area-grow');
777
778
			if(!$record->canEdit() || $deletedFromStage) {
779
				$readonlyFields = $form->Fields()->makeReadonly();
780
				$form->setFields($readonlyFields);
781
			}
782
783
			$form->Fields()->setForm($form);
784
785
			$this->extend('updateEditForm', $form);
786
			return $form;
787
		} else if($id) {
788
			$form = Form::create( $this, "EditForm", new FieldList(
789
				new LabelField('PageDoesntExistLabel',_t('CMSMain.PAGENOTEXISTS',"This page doesn't exist"))), new FieldList()
790
			)->setHTMLID('Form_EditForm');
791
			return $form;
792
		}
793
	}
794
795
	/**
796
	 * @param HTTPRequest $request
797
	 * @return string HTML
798
	 */
799
	public function treeview($request) {
800
		return $this->getResponseNegotiator()->respond($request);
801
	}
802
803
	/**
804
	 * @param HTTPRequest $request
805
	 * @return string HTML
806
	 */
807
	public function listview($request) {
808
		return $this->getResponseNegotiator()->respond($request);
809
	}
810
811
	/**
812
	 * @return string
813
	 */
814
	public function ViewState() {
815
		$mode = $this->getRequest()->requestVar('view')
816
			?: $this->getRequest()->param('Action');
817
		switch($mode) {
818
			case 'listview':
819
			case 'treeview':
820
				return $mode;
821
			default:
822
				return 'treeview';
823
		}
824
	}
825
826
	/**
827
	 * Callback to request the list of page types allowed under a given page instance.
828
	 * Provides a slower but more precise response over SiteTreeHints
829
	 *
830
	 * @param HTTPRequest $request
831
	 * @return HTTPResponse
832
	 */
833
	public function childfilter($request) {
834
		// Check valid parent specified
835
		$parentID = $request->requestVar('ParentID');
836
		$parent = SiteTree::get()->byID($parentID);
837
		if(!$parent || !$parent->exists()) return $this->httpError(404);
838
839
		// Build hints specific to this class
840
		// Identify disallows and set globals
841
		$classes = SiteTree::page_type_classes();
842
		$disallowedChildren = array();
843
		foreach($classes as $class) {
844
			$obj = singleton($class);
845
			if($obj instanceof HiddenClass) continue;
846
847
			if(!$obj->canCreate(null, array('Parent' => $parent))) {
848
				$disallowedChildren[] = $class;
849
			}
850
		}
851
852
		$this->extend('updateChildFilter', $disallowedChildren, $parentID);
853
		return $this
854
			->getResponse()
855
			->addHeader('Content-Type', 'application/json; charset=utf-8')
856
			->setBody(Convert::raw2json($disallowedChildren));
857
	}
858
859
	/**
860
	 * Safely reconstruct a selected filter from a given set of query parameters
861
	 *
862
	 * @param array $params Query parameters to use
863
	 * @return CMSSiteTreeFilter The filter class, or null if none present
864
	 * @throws InvalidArgumentException if invalid filter class is passed.
865
	 */
866
	protected function getQueryFilter($params) {
867
		if(empty($params['FilterClass'])) return null;
868
		$filterClass = $params['FilterClass'];
869
		if(!is_subclass_of($filterClass, 'SilverStripe\\CMS\\Controllers\\CMSSiteTreeFilter')) {
870
			throw new InvalidArgumentException("Invalid filter class passed: {$filterClass}");
871
		}
872
		return $filterClass::create($params);
873
	}
874
875
	/**
876
	 * Returns the pages meet a certain criteria as {@see CMSSiteTreeFilter} or the subpages of a parent page
877
	 * defaulting to no filter and show all pages in first level.
878
	 * Doubles as search results, if any search parameters are set through {@link SearchForm()}.
879
	 *
880
	 * @param array $params Search filter criteria
881
	 * @param int $parentID Optional parent node to filter on (can't be combined with other search criteria)
882
	 * @return SS_List
883
	 * @throws InvalidArgumentException if invalid filter class is passed.
884
	 */
885
	public function getList($params = array(), $parentID = 0) {
886
		if($filter = $this->getQueryFilter($params)) {
887
			return $filter->getFilteredPages();
888
		} else {
889
			$list = DataList::create($this->stat('tree_class'));
890
			$parentID = is_numeric($parentID) ? $parentID : 0;
891
			return $list->filter("ParentID", $parentID);
892
		}
893
	}
894
895
	/**
896
	 * @return Form
897
	 */
898
	public function ListViewForm() {
899
		$params = $this->getRequest()->requestVar('q');
900
		$list = $this->getList($params, $parentID = $this->getRequest()->requestVar('ParentID'));
901
		$gridFieldConfig = GridFieldConfig::create()->addComponents(
902
			new GridFieldSortableHeader(),
903
			new GridFieldDataColumns(),
904
			new GridFieldPaginator(self::config()->page_length)
905
		);
906
		if($parentID){
907
			$linkSpec = $this->Link();
908
			$linkSpec = $linkSpec . (strstr($linkSpec, '?') ? '&' : '?') . 'ParentID=%d&view=listview';
909
			$gridFieldConfig->addComponent(
910
				GridFieldLevelup::create($parentID)
911
					->setLinkSpec($linkSpec)
912
					->setAttributes(array('data-pjax' => 'ListViewForm,Breadcrumbs'))
913
			);
914
		}
915
		$gridField = new GridField('Page','Pages', $list, $gridFieldConfig);
916
		/** @var GridFieldDataColumns $columns */
917
		$columns = $gridField->getConfig()->getComponentByType('SilverStripe\\Forms\\GridField\\GridFieldDataColumns');
918
919
		// Don't allow navigating into children nodes on filtered lists
920
		$fields = array(
921
			'getTreeTitle' => _t('SiteTree.PAGETITLE', 'Page Title'),
922
			'singular_name' => _t('SiteTree.PAGETYPE', 'Page Type'),
923
			'LastEdited' => _t('SiteTree.LASTUPDATED', 'Last Updated'),
924
		);
925
		/** @var GridFieldSortableHeader $sortableHeader */
926
		$sortableHeader = $gridField->getConfig()->getComponentByType('SilverStripe\\Forms\\GridField\\GridFieldSortableHeader');
927
		$sortableHeader->setFieldSorting(array('getTreeTitle' => 'Title'));
928
		$gridField->getState()->ParentID = $parentID;
929
930
		if(!$params) {
931
			$fields = array_merge(array('listChildrenLink' => ''), $fields);
932
		}
933
934
		$columns->setDisplayFields($fields);
935
		$columns->setFieldCasting(array(
936
			'Created' => 'DBDatetime->Ago',
937
			'LastEdited' => 'DBDatetime->FormatFromSettings',
938
			'getTreeTitle' => 'HTMLFragment'
939
		));
940
941
		$controller = $this;
942
		$columns->setFieldFormatting(array(
943
			'listChildrenLink' => function($value, &$item) use($controller) {
944
				/** @var SiteTree $item */
945
				$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...
946
				if($num) {
947
					return sprintf(
948
						'<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>',
949
						Controller::join_links(
950
							$controller->Link(),
951
							sprintf("?ParentID=%d&view=listview", (int)$item->ID)
952
						),
953
						$num
954
					);
955
				}
956
			},
957
			'getTreeTitle' => function($value, &$item) use($controller) {
958
				return sprintf(
959
					'<a class="action-detail" href="%s">%s</a>',
960
					Controller::join_links(
961
						CMSPageEditController::singleton()->Link('show'),
962
						(int)$item->ID
963
					),
964
					$item->TreeTitle // returns HTML, does its own escaping
965
				);
966
			}
967
		));
968
969
		$negotiator = $this->getResponseNegotiator();
970
		$listview = Form::create(
971
			$this,
972
			'ListViewForm',
973
			new FieldList($gridField),
974
			new FieldList()
975
		)->setHTMLID('Form_ListViewForm');
976
		$listview->setAttribute('data-pjax-fragment', 'ListViewForm');
977 View Code Duplication
		$listview->setValidationResponseCallback(function(ValidationResult $errors) use ($negotiator, $listview) {
0 ignored issues
show
Unused Code introduced by
The parameter $errors is not used and could be removed.

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

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

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

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

Loading history...
978
			$request = $this->getRequest();
979
			if($request->isAjax() && $negotiator) {
980
				$result = $listview->forTemplate();
981
				return $negotiator->respond($request, array(
982
					'CurrentForm' => function() use($result) {
983
						return $result;
984
					}
985
				));
986
			}
987
		});
988
989
		$this->extend('updateListView', $listview);
990
991
		$listview->disableSecurityToken();
992
		return $listview;
993
	}
994
995
	public function currentPageID() {
996
		$id = parent::currentPageID();
997
998
		$this->extend('updateCurrentPageID', $id);
999
1000
		return $id;
1001
	}
1002
1003
	//------------------------------------------------------------------------------------------//
1004
	// Data saving handlers
1005
1006
	/**
1007
	 * Save and Publish page handler
1008
	 *
1009
	 * @param array $data
1010
	 * @param Form $form
1011
	 * @return HTTPResponse
1012
	 * @throws HTTPResponse_Exception
1013
	 */
1014
	public function save($data, $form) {
1015
		$className = $this->stat('tree_class');
1016
1017
		// Existing or new record?
1018
		$id = $data['ID'];
1019
		if(substr($id,0,3) != 'new') {
1020
			/** @var SiteTree $record */
1021
			$record = DataObject::get_by_id($className, $id);
1022
			// Check edit permissions
1023
			if($record && !$record->canEdit()) {
1024
				return Security::permissionFailure($this);
1025
			}
1026
			if(!$record || !$record->ID) {
1027
				throw new HTTPResponse_Exception("Bad record ID #$id", 404);
1028
			}
1029
		} else {
1030
			if(!$className::singleton()->canCreate()) {
1031
				return Security::permissionFailure($this);
1032
			}
1033
			$record = $this->getNewItem($id, false);
1034
		}
1035
1036
		// Check publishing permissions
1037
		$doPublish = !empty($data['publish']);
1038
		if($record && $doPublish && !$record->canPublish()) {
1039
			return Security::permissionFailure($this);
1040
		}
1041
1042
		// TODO Coupling to SiteTree
1043
		$record->HasBrokenLink = 0;
1044
		$record->HasBrokenFile = 0;
1045
1046
		if (!$record->ObsoleteClassName) {
1047
			$record->writeWithoutVersion();
1048
		}
1049
1050
		// Update the class instance if necessary
1051
		if(isset($data['ClassName']) && $data['ClassName'] != $record->ClassName) {
1052
			// Replace $record with a new instance of the new class
1053
			$newClassName = $data['ClassName'];
1054
			$record = $record->newClassInstance($newClassName);
1055
		}
1056
1057
		// save form data into record
1058
		$form->saveInto($record);
1059
		$record->write();
1060
1061
		// If the 'Save & Publish' button was clicked, also publish the page
1062
		if ($doPublish) {
1063
			$record->publishRecursive();
1064
			$message = _t(
1065
				'CMSMain.PUBLISHED',
1066
				"Published '{title}' successfully.",
1067
				['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...
1068
			);
1069
		} else {
1070
			$message = _t(
1071
				'CMSMain.SAVED',
1072
				"Saved '{title}' successfully.",
1073
				['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...
1074
			);
1075
		}
1076
1077
		$this->getResponse()->addHeader('X-Status', rawurlencode($message));
1078
		return $this->getResponseNegotiator()->respond($this->getRequest());
1079
	}
1080
1081
	/**
1082
	 * @uses LeftAndMainExtension->augmentNewSiteTreeItem()
1083
	 *
1084
	 * @param int|string $id
1085
	 * @param bool $setID
1086
	 * @return mixed|DataObject
1087
	 * @throws HTTPResponse_Exception
1088
	 */
1089
	public function getNewItem($id, $setID = true) {
1090
		$parentClass = $this->stat('tree_class');
1091
		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...
1092
1093
		if (!is_a($className, $parentClass, true)) {
1094
			$response = Security::permissionFailure($this);
1095
			if (!$response) {
1096
				$response = $this->getResponse();
1097
			}
1098
			throw new HTTPResponse_Exception($response);
1099
		}
1100
1101
		/** @var SiteTree $newItem */
1102
		$newItem = Injector::inst()->create($className);
1103
		if( !$suffix ) {
1104
			$sessionTag = "NewItems." . $parentID . "." . $className;
1105
			if(Session::get($sessionTag)) {
1106
				$suffix = '-' . Session::get($sessionTag);
1107
				Session::set($sessionTag, Session::get($sessionTag) + 1);
1108
			}
1109
			else
1110
				Session::set($sessionTag, 1);
1111
1112
				$id = $id . $suffix;
1113
		}
1114
1115
		$newItem->Title = _t(
1116
			'CMSMain.NEWPAGE',
1117
			"New {pagetype}",'followed by a page type title',
1118
			array('pagetype' => singleton($className)->i18n_singular_name())
1119
		);
1120
		$newItem->ClassName = $className;
1121
		$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...
1122
1123
		// DataObject::fieldExists only checks the current class, not the hierarchy
1124
		// This allows the CMS to set the correct sort value
1125
		if($newItem->castingHelper('Sort')) {
1126
			$newItem->Sort = DB::prepared_query('SELECT MAX("Sort") FROM "SiteTree" WHERE "ParentID" = ?', array($parentID))->value() + 1;
1127
		}
1128
1129
		if($setID) $newItem->ID = $id;
1130
1131
		# Some modules like subsites add extra fields that need to be set when the new item is created
1132
		$this->extend('augmentNewSiteTreeItem', $newItem);
1133
1134
		return $newItem;
1135
	}
1136
1137
	/**
1138
	 * Actually perform the publication step
1139
	 *
1140
	 * @param Versioned|DataObject $record
1141
	 * @return mixed
1142
	 */
1143
	public function performPublish($record) {
1144
		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...
1145
			return Security::permissionFailure($this);
1146
		}
1147
1148
		$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...
1149
	}
1150
1151
	/**
1152
	 * Reverts a page by publishing it to live.
1153
	 * Use {@link restorepage()} if you want to restore a page
1154
	 * which was deleted from draft without publishing.
1155
	 *
1156
	 * @uses SiteTree->doRevertToLive()
1157
	 *
1158
	 * @param array $data
1159
	 * @param Form $form
1160
	 * @return HTTPResponse
1161
	 * @throws HTTPResponse_Exception
1162
	 */
1163
	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...
1164
		if(!isset($data['ID'])) {
1165
			throw new HTTPResponse_Exception("Please pass an ID in the form content", 400);
1166
		}
1167
1168
		$id = (int) $data['ID'];
1169
		$restoredPage = Versioned::get_latest_version(SiteTree::class, $id);
1170
		if(!$restoredPage) {
1171
			throw new HTTPResponse_Exception("SiteTree #$id not found", 400);
1172
		}
1173
1174
		/** @var SiteTree $record */
1175
		$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...
1176
			'"SiteTree_Live"."ID"' => $id
1177
		));
1178
1179
		// a user can restore a page without publication rights, as it just adds a new draft state
1180
		// (this action should just be available when page has been "deleted from draft")
1181
		if($record && !$record->canEdit()) {
1182
			return Security::permissionFailure($this);
1183
		}
1184
		if(!$record || !$record->ID) {
1185
			throw new HTTPResponse_Exception("Bad record ID #$id", 404);
1186
		}
1187
1188
		$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...
1189
1190
		$this->getResponse()->addHeader(
1191
			'X-Status',
1192
			rawurlencode(_t(
1193
				'CMSMain.RESTORED',
1194
				"Restored '{title}' successfully",
1195
				'Param %s is a title',
1196
				array('title' => $record->Title)
1197
			))
1198
		);
1199
1200
		return $this->getResponseNegotiator()->respond($this->getRequest());
1201
	}
1202
1203
	/**
1204
	 * Delete the current page from draft stage.
1205
	 *
1206
	 * @see deletefromlive()
1207
	 *
1208
	 * @param array $data
1209
	 * @param Form $form
1210
	 * @return HTTPResponse
1211
	 * @throws HTTPResponse_Exception
1212
	 */
1213 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...
1214
		$id = $data['ID'];
1215
		$record = SiteTree::get()->byID($id);
1216
		if($record && !$record->canDelete()) {
1217
			return Security::permissionFailure();
1218
		}
1219
		if(!$record || !$record->ID) {
1220
			throw new HTTPResponse_Exception("Bad record ID #$id", 404);
1221
		}
1222
1223
		// Delete record
1224
		$record->delete();
1225
1226
		$this->getResponse()->addHeader(
1227
			'X-Status',
1228
			rawurlencode(sprintf(_t('CMSMain.REMOVEDPAGEFROMDRAFT',"Removed '%s' from the draft site"), $record->Title))
1229
		);
1230
1231
		// Even if the record has been deleted from stage and live, it can be viewed in "archive mode"
1232
		return $this->getResponseNegotiator()->respond($this->getRequest());
1233
	}
1234
1235
	/**
1236
	 * Delete this page from both live and stage
1237
	 *
1238
	 * @param array $data
1239
	 * @param Form $form
1240
	 * @return HTTPResponse
1241
	 * @throws HTTPResponse_Exception
1242
	 */
1243 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...
1244
		$id = $data['ID'];
1245
		/** @var SiteTree $record */
1246
		$record = SiteTree::get()->byID($id);
1247
		if(!$record || !$record->exists()) {
1248
			throw new HTTPResponse_Exception("Bad record ID #$id", 404);
1249
		}
1250
		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...
1251
			return Security::permissionFailure();
1252
		}
1253
1254
		// Archive record
1255
		$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...
1256
1257
		$this->getResponse()->addHeader(
1258
			'X-Status',
1259
			rawurlencode(sprintf(_t('CMSMain.ARCHIVEDPAGE',"Archived page '%s'"), $record->Title))
1260
		);
1261
1262
		// Even if the record has been deleted from stage and live, it can be viewed in "archive mode"
1263
		return $this->getResponseNegotiator()->respond($this->getRequest());
1264
	}
1265
1266
	public function publish($data, $form) {
1267
		$data['publish'] = '1';
1268
1269
		return $this->save($data, $form);
1270
	}
1271
1272
	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...
1273
		$className = $this->stat('tree_class');
1274
		/** @var SiteTree $record */
1275
		$record = DataObject::get_by_id($className, $data['ID']);
1276
1277
		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...
1278
			return Security::permissionFailure($this);
1279
		}
1280
		if(!$record || !$record->ID) {
1281
			throw new HTTPResponse_Exception("Bad record ID #" . (int)$data['ID'], 404);
1282
		}
1283
1284
		$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...
1285
1286
		$this->getResponse()->addHeader(
1287
			'X-Status',
1288
			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...
1289
		);
1290
1291
		return $this->getResponseNegotiator()->respond($this->getRequest());
1292
	}
1293
1294
	/**
1295
	 * @return HTTPResponse
1296
	 */
1297
	public function rollback() {
1298
		return $this->doRollback(array(
1299
			'ID' => $this->currentPageID(),
1300
			'Version' => $this->getRequest()->param('VersionID')
1301
		), 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...
1302
	}
1303
1304
	/**
1305
	 * Rolls a site back to a given version ID
1306
	 *
1307
	 * @param array $data
1308
	 * @param Form $form
1309
	 * @return HTTPResponse
1310
	 */
1311
	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...
1312
		$this->extend('onBeforeRollback', $data['ID']);
1313
1314
		$id = (isset($data['ID'])) ? (int) $data['ID'] : null;
1315
		$version = (isset($data['Version'])) ? (int) $data['Version'] : null;
1316
1317
		/** @var DataObject|Versioned $record */
1318
		$record = DataObject::get_by_id($this->stat('tree_class'), $id);
1319
		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...
1320
			return Security::permissionFailure($this);
1321
		}
1322
1323
		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...
1324
			$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...
1325
			$message = _t(
1326
				'CMSMain.ROLLEDBACKVERSIONv2',
1327
				"Rolled back to version #%d.",
1328
				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...
1329
			);
1330
		} else {
1331
			$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...
1332
			$message = _t(
1333
				'CMSMain.ROLLEDBACKPUBv2',"Rolled back to published version."
1334
			);
1335
		}
1336
1337
		$this->getResponse()->addHeader('X-Status', rawurlencode($message));
1338
1339
		// Can be used in different contexts: In normal page edit view, in which case the redirect won't have any effect.
1340
		// Or in history view, in which case a revert causes the CMS to re-load the edit view.
1341
		// The X-Pjax header forces a "full" content refresh on redirect.
1342
		$url = Controller::join_links(CMSPageEditController::singleton()->Link('show'), $record->ID);
1343
		$this->getResponse()->addHeader('X-ControllerURL', $url);
1344
		$this->getRequest()->addHeader('X-Pjax', 'Content');
1345
		$this->getResponse()->addHeader('X-Pjax', 'Content');
1346
1347
		return $this->getResponseNegotiator()->respond($this->getRequest());
1348
	}
1349
1350
	/**
1351
	 * Batch Actions Handler
1352
	 */
1353
	public function batchactions() {
1354
		return new CMSBatchActionHandler($this, 'batchactions');
1355
	}
1356
1357
	public function BatchActionParameters() {
1358
		$batchActions = CMSBatchActionHandler::config()->batch_actions;
1359
1360
		$forms = array();
1361
		foreach($batchActions as $urlSegment => $batchAction) {
1362
			$SNG_action = singleton($batchAction);
1363
			if ($SNG_action->canView() && $fieldset = $SNG_action->getParameterFields()) {
1364
				$formHtml = '';
1365
				/** @var FormField $field */
1366
				foreach($fieldset as $field) {
1367
					$formHtml .= $field->Field();
1368
				}
1369
				$forms[$urlSegment] = $formHtml;
1370
			}
1371
		}
1372
		$pageHtml = '';
1373
		foreach($forms as $urlSegment => $html) {
1374
			$pageHtml .= "<div class=\"params\" id=\"BatchActionParameters_$urlSegment\">$html</div>\n\n";
1375
		}
1376
		return new LiteralField("BatchActionParameters", '<div id="BatchActionParameters" style="display:none">'.$pageHtml.'</div>');
1377
	}
1378
	/**
1379
	 * Returns a list of batch actions
1380
	 */
1381
	public function BatchActionList() {
1382
		return $this->batchactions()->batchActionList();
1383
	}
1384
1385
	public function publishall($request) {
1386
		if(!Permission::check('ADMIN')) return Security::permissionFailure($this);
1387
1388
		increase_time_limit_to();
1389
		increase_memory_limit_to();
1390
1391
		$response = "";
1392
1393
		if(isset($this->requestParams['confirm'])) {
1394
			// Protect against CSRF on destructive action
1395
			if(!SecurityToken::inst()->checkRequest($request)) return $this->httpError(400);
1396
1397
			$start = 0;
1398
			$pages = SiteTree::get()->limit("$start,30");
1399
			$count = 0;
1400
			while($pages) {
1401
				/** @var SiteTree $page */
1402
				foreach($pages as $page) {
1403
					if($page && !$page->canPublish()) {
1404
						return Security::permissionFailure($this);
1405
					}
1406
1407
					$page->publishRecursive();
1408
					$page->destroy();
1409
					unset($page);
1410
					$count++;
1411
					$response .= "<li>$count</li>";
1412
				}
1413
				if($pages->count() > 29) {
1414
					$start += 30;
1415
					$pages = SiteTree::get()->limit("$start,30");
1416
				} else {
1417
					break;
1418
				}
1419
			}
1420
			$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...
1421
1422
		} else {
1423
			$token = SecurityToken::inst();
1424
			$fields = new FieldList();
1425
			$token->updateFieldSet($fields);
1426
			$tokenField = $fields->first();
1427
			$tokenHtml = ($tokenField) ? $tokenField->FieldHolder() : '';
1428
			$publishAllDescription = _t(
1429
				'CMSMain.PUBALLFUN2',
1430
				'Pressing this button will do the equivalent of going to every page and pressing "publish".  '
1431
				. 'It\'s intended to be used after there have been massive edits of the content, such as when '
1432
				. 'the site was first built.'
1433
			);
1434
			$response .= '<h1>' . _t('CMSMain.PUBALLFUN','"Publish All" functionality') . '</h1>
1435
				<p>' . $publishAllDescription . '</p>
1436
				<form method="post" action="publishall">
1437
					<input type="submit" name="confirm" value="'
1438
					. _t('CMSMain.PUBALLCONFIRM',"Please publish every page in the site, copying content stage to live",'Confirmation button') .'" />'
1439
					. $tokenHtml .
1440
				'</form>';
1441
		}
1442
1443
		return $response;
1444
	}
1445
1446
	/**
1447
	 * Restore a completely deleted page from the SiteTree_versions table.
1448
	 *
1449
	 * @param array $data
1450
	 * @param Form $form
1451
	 * @return HTTPResponse
1452
	 */
1453
	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...
1454
		if(!isset($data['ID']) || !is_numeric($data['ID'])) {
1455
			return new HTTPResponse("Please pass an ID in the form content", 400);
1456
		}
1457
1458
		$id = (int)$data['ID'];
1459
		/** @var SiteTree $restoredPage */
1460
		$restoredPage = Versioned::get_latest_version(SiteTree::class, $id);
1461
		if(!$restoredPage) {
1462
			return new HTTPResponse("SiteTree #$id not found", 400);
1463
		}
1464
1465
		$restoredPage = $restoredPage->doRestoreToStage();
1466
1467
		$this->getResponse()->addHeader(
1468
			'X-Status',
1469
			rawurlencode(_t(
1470
				'CMSMain.RESTORED',
1471
				"Restored '{title}' successfully",
1472
				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...
1473
			))
1474
		);
1475
1476
		return $this->getResponseNegotiator()->respond($this->getRequest());
1477
	}
1478
1479
	public function duplicate($request) {
1480
		// Protect against CSRF on destructive action
1481
		if(!SecurityToken::inst()->checkRequest($request)) return $this->httpError(400);
1482
1483
		if(($id = $this->urlParams['ID']) && is_numeric($id)) {
1484
			/** @var SiteTree $page */
1485
			$page = SiteTree::get()->byID($id);
1486 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...
1487
				return Security::permissionFailure($this);
1488
			}
1489
			if(!$page || !$page->ID) throw new HTTPResponse_Exception("Bad record ID #$id", 404);
1490
1491
			$newPage = $page->duplicate();
1492
1493
			// ParentID can be hard-set in the URL.  This is useful for pages with multiple parents
1494
			if(isset($_GET['parentID']) && is_numeric($_GET['parentID'])) {
1495
				$newPage->ParentID = $_GET['parentID'];
1496
				$newPage->write();
1497
			}
1498
1499
			$this->getResponse()->addHeader(
1500
				'X-Status',
1501
				rawurlencode(_t(
1502
					'CMSMain.DUPLICATED',
1503
					"Duplicated '{title}' successfully",
1504
					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...
1505
				))
1506
			);
1507
			$url = Controller::join_links(CMSPageEditController::singleton()->Link('show'), $newPage->ID);
1508
			$this->getResponse()->addHeader('X-ControllerURL', $url);
1509
			$this->getRequest()->addHeader('X-Pjax', 'Content');
1510
			$this->getResponse()->addHeader('X-Pjax', 'Content');
1511
1512
			return $this->getResponseNegotiator()->respond($this->getRequest());
1513
		} else {
1514
			return new HTTPResponse("CMSMain::duplicate() Bad ID: '$id'", 400);
1515
		}
1516
	}
1517
1518
	public function duplicatewithchildren($request) {
1519
		// Protect against CSRF on destructive action
1520
		if(!SecurityToken::inst()->checkRequest($request)) return $this->httpError(400);
1521
		increase_time_limit_to();
1522
		if(($id = $this->urlParams['ID']) && is_numeric($id)) {
1523
			/** @var SiteTree $page */
1524
			$page = SiteTree::get()->byID($id);
1525 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...
1526
				return Security::permissionFailure($this);
1527
			}
1528
			if(!$page || !$page->ID) throw new HTTPResponse_Exception("Bad record ID #$id", 404);
1529
1530
			$newPage = $page->duplicateWithChildren();
1531
1532
			$this->getResponse()->addHeader(
1533
				'X-Status',
1534
				rawurlencode(_t(
1535
					'CMSMain.DUPLICATEDWITHCHILDREN',
1536
					"Duplicated '{title}' and children successfully",
1537
					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...
1538
				))
1539
			);
1540
			$url = Controller::join_links(CMSPageEditController::singleton()->Link('show'), $newPage->ID);
1541
			$this->getResponse()->addHeader('X-ControllerURL', $url);
1542
			$this->getRequest()->addHeader('X-Pjax', 'Content');
1543
			$this->getResponse()->addHeader('X-Pjax', 'Content');
1544
1545
			return $this->getResponseNegotiator()->respond($this->getRequest());
1546
		} else {
1547
			return new HTTPResponse("CMSMain::duplicatewithchildren() Bad ID: '$id'", 400);
1548
		}
1549
	}
1550
1551
	public function providePermissions() {
1552
		$title = CMSPagesController::menu_title();
1553
		return array(
1554
			"CMS_ACCESS_CMSMain" => array(
1555
				'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...
1556
				'category' => _t('Permission.CMS_ACCESS_CATEGORY', 'CMS Access'),
1557
				'help' => _t(
1558
					'CMSMain.ACCESS_HELP',
1559
					'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".'
1560
				),
1561
				'sort' => -99 // below "CMS_ACCESS_LeftAndMain", but above everything else
1562
			)
1563
		);
1564
	}
1565
1566
}
1567