Completed
Push — master ( 25a8fb...29fc7a )
by Hamish
12s
created

CMSMain::doRollback()   B

Complexity

Conditions 6
Paths 12

Size

Total Lines 38
Code Lines 23

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
dl 0
loc 38
rs 8.439
c 1
b 0
f 0
cc 6
eloc 23
nc 12
nop 2
1
<?php
2
3
namespace SilverStripe\CMS\Controllers;
4
5
use ResetFormAction;
6
use SilverStripe\ORM\FieldType\DBHTMLText;
7
use SilverStripe\ORM\SS_List;
8
use SilverStripe\ORM\Versioning\Versioned;
9
use SilverStripe\ORM\HiddenClass;
10
use SilverStripe\ORM\ArrayList;
11
use SilverStripe\ORM\DataObject;
12
use SilverStripe\ORM\DataList;
13
use SilverStripe\ORM\DB;
14
use SilverStripe\Security\Member;
15
use SilverStripe\Security\Security;
16
use SilverStripe\Security\SecurityToken;
17
use SilverStripe\Security\Permission;
18
use SilverStripe\Security\PermissionProvider;
19
use LeftAndMain;
20
21
22
use SS_HTTPRequest;
23
use Translatable;
24
use Requirements;
25
use CMSBatchActionHandler;
26
use Controller;
27
use AdminRootController;
28
use Director;
29
use Page;
30
31
use TextField;
32
use HeaderField;
33
use DateField;
34
use DropdownField;
35
use FieldGroup;
36
use FieldList;
37
use FormAction;
38
use Object;
39
use Form;
40
use SS_Cache;
41
use Zend_Cache;
42
use Convert;
43
use ArrayData;
44
use HiddenField;
45
use CMSPreviewable;
46
use LiteralField;
47
use RequiredFields;
48
use LabelField;
49
use InvalidArgumentException;
50
use GridFieldConfig;
51
use GridFieldSortableHeader;
52
use GridFieldDataColumns;
53
use GridFieldPaginator;
54
use GridFieldLevelup;
55
use GridField;
56
use SS_HTTPResponse_Exception;
57
use Session;
58
use AddToCampaignHandler;
59
use HTMLEditorField;
60
use SS_HTTPResponse;
61
use SilverStripe\CMS\Model\SiteTree;
62
use SilverStripe\CMS\Model\RedirectorPage;
63
use SilverStripe\CMS\Model\CurrentPageIdentifier;
64
65
66
67
68
69
/**
70
 * The main "content" area of the CMS.
71
 *
72
 * This class creates a 2-frame layout - left-tree and right-form - to sit beneath the main
73
 * admin menu.
74
 *
75
 * @package cms
76
 * @subpackage controller
77
 * @todo Create some base classes to contain the generic functionality that will be replicated.
78
 */
79
class CMSMain extends LeftAndMain implements CurrentPageIdentifier, PermissionProvider {
80
81
	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...
82
83
	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...
84
85
	// Maintain a lower priority than other administration sections
86
	// so that Director does not think they are actions of CMSMain
87
	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...
88
89
	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...
90
91
	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...
92
93
	private static $tree_class = "SilverStripe\\CMS\\Model\\SiteTree";
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...
94
95
	private static $subitem_class = "SilverStripe\\Security\\Member";
96
97
	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...
98
99
	/**
100
	 * Amount of results showing on a single page.
101
	 *
102
	 * @config
103
	 * @var int
104
	 */
105
	private static $page_length = 15;
106
107
	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...
108
		'archive',
109
		'deleteitems',
110
		'DeleteItemsForm',
111
		'dialog',
112
		'duplicate',
113
		'duplicatewithchildren',
114
		'publishall',
115
		'publishitems',
116
		'PublishItemsForm',
117
		'submit',
118
		'EditForm',
119
		'SearchForm',
120
		'SiteTreeAsUL',
121
		'getshowdeletedsubtree',
122
		'batchactions',
123
		'treeview',
124
		'listview',
125
		'ListViewForm',
126
		'childfilter',
127
	);
128
129
	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...
130
		'TreeIsFiltered' => 'Boolean',
131
		'AddForm' => 'HTMLFragment',
132
		'LinkPages' => 'Text',
133
		'Link' => 'Text',
134
		'ListViewForm' => 'HTMLFragment',
135
		'ExtraTreeTools' => 'HTMLFragment',
136
		'SiteTreeHints' => 'HTMLFragment',
137
		'SecurityID' => 'Text',
138
		'SiteTreeAsUL' => 'HTMLFragment',
139
	);
140
141
	public function init() {
142
		// set reading lang
143
		if(SiteTree::has_extension('Translatable') && !$this->getRequest()->isAjax()) {
144
			Translatable::choose_site_locale(array_keys(Translatable::get_existing_content_languages('SilverStripe\\CMS\\Model\\SiteTree')));
145
		}
146
147
		parent::init();
148
149
		Requirements::css(CMS_DIR . '/client/dist/styles/bundle.css');
150
		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...
151
		Requirements::add_i18n_javascript(CMS_DIR . '/client/lang', false, true);
152
		Requirements::javascript(CMS_DIR . '/client/dist/js/bundle-legacy.js', [
153
			'provides' => [
154
				CMS_DIR . '/client/dist/js/CMSMain.AddForm.js',
155
				CMS_DIR . '/client/dist/js/CMSMain.EditForm.js',
156
				CMS_DIR . '/client/dist/js/CMSMain.js',
157
				CMS_DIR . '/client/dist/js/CMSMain.Tree.js',
158
				CMS_DIR . '/client/dist/js/CMSPageHistoryController.js',
159
				CMS_DIR . '/client/dist/js/RedirectorPage.js',
160
				CMS_DIR . '/client/dist/js/SilverStripeNavigator.js',
161
				CMS_DIR . '/client/dist/js/SiteTreeURLSegmentField.js'
162
			]
163
		]);
164
165
		CMSBatchActionHandler::register('publish', 'SilverStripe\\CMS\\BatchActions\\CMSBatchAction_Publish');
166
		CMSBatchActionHandler::register('unpublish', 'SilverStripe\\CMS\\BatchActions\\CMSBatchAction_Unpublish');
167
		CMSBatchActionHandler::register('delete', 'SilverStripe\\CMS\\BatchActions\\CMSBatchAction_Delete');
168
		CMSBatchActionHandler::register('archive', 'SilverStripe\\CMS\\BatchActions\\CMSBatchAction_Archive');
169
		CMSBatchActionHandler::register('restore', 'SilverStripe\\CMS\\BatchActions\\CMSBatchAction_Restore');
170
	}
171
172
	public function index($request) {
173
		// In case we're not showing a specific record, explicitly remove any session state,
174
		// to avoid it being highlighted in the tree, and causing an edit form to show.
175
		if(!$request->param('Action')) $this->setCurrentPageId(null);
176
177
		return parent::index($request);
178
	}
179
180
	public function getResponseNegotiator() {
181
		$negotiator = parent::getResponseNegotiator();
182
		$controller = $this;
183
		$negotiator->setCallback('ListViewForm', function() use(&$controller) {
184
			return $controller->ListViewForm()->forTemplate();
185
		});
186
		return $negotiator;
187
	}
188
189
	/**
190
	 * If this is set to true, the "switchView" context in the
191
	 * template is shown, with links to the staging and publish site.
192
	 *
193
	 * @return boolean
194
	 */
195
	public function ShowSwitchView() {
196
		return true;
197
	}
198
199
	/**
200
	 * Overloads the LeftAndMain::ShowView. Allows to pass a page as a parameter, so we are able
201
	 * to switch view also for archived versions.
202
	 */
203
	public function SwitchView($page = null) {
204
		if(!$page) {
205
			$page = $this->currentPage();
206
		}
207
208
		if($page) {
209
			$nav = SilverStripeNavigator::get_for_record($page);
210
			return $nav['items'];
211
		}
212
	}
213
214
	//------------------------------------------------------------------------------------------//
215
	// Main controllers
216
217
	//------------------------------------------------------------------------------------------//
218
	// Main UI components
219
220
	/**
221
	 * Override {@link LeftAndMain} Link to allow blank URL segment for CMSMain.
222
	 *
223
	 * @param string|null $action Action to link to.
224
	 * @return string
225
	 */
226
	public function Link($action = null) {
227
		$link = Controller::join_links(
228
			AdminRootController::admin_url(),
229
			$this->stat('url_segment', true), // in case we want to change the segment
0 ignored issues
show
Unused Code introduced by
The call to CMSMain::stat() has too many arguments starting with true.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
230
			'/', // trailing slash needed if $action is null!
231
			"$action"
232
		);
233
		$this->extend('updateLink', $link);
234
		return $link;
235
	}
236
237
	public function LinkPages() {
238
		return CMSPagesController::singleton()->Link();
239
	}
240
241
	public function LinkPagesWithSearch() {
242
		return $this->LinkWithSearch($this->LinkPages());
243
	}
244
245
	public function LinkTreeView() {
246
		return $this->LinkWithSearch($this->Link('treeview'));
247
	}
248
249
	public function LinkListView() {
250
		return $this->LinkWithSearch($this->Link('listview'));
251
	}
252
253
	public function LinkGalleryView() {
254
		return $this->LinkWithSearch($this->Link('galleryview'));
255
	}
256
257 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...
258
		if(!$id) {
259
			$id = $this->currentPageID();
260
		}
261
		return $this->LinkWithSearch(
262
			Controller::join_links(CMSPageEditController::singleton()->Link('show'), $id)
263
		);
264
	}
265
266 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...
267
		if($id = $this->currentPageID()) {
268
			return $this->LinkWithSearch(
269
				Controller::join_links(CMSPageSettingsController::singleton()->Link('show'), $id)
270
			);
271
		} else {
272
			return null;
273
		}
274
	}
275
276 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...
277
		if($id = $this->currentPageID()) {
278
			return $this->LinkWithSearch(
279
				Controller::join_links(CMSPageHistoryController::singleton()->Link('show'), $id)
280
			);
281
		} else {
282
			return null;
283
		}
284
	}
285
286
	public function LinkWithSearch($link) {
287
		// Whitelist to avoid side effects
288
		$params = array(
289
			'q' => (array)$this->getRequest()->getVar('q'),
290
			'ParentID' => $this->getRequest()->getVar('ParentID')
291
		);
292
		$link = Controller::join_links(
293
			$link,
294
			array_filter(array_values($params)) ? '?' . http_build_query($params) : null
295
		);
296
		$this->extend('updateLinkWithSearch', $link);
297
		return $link;
298
	}
299
300
	public function LinkPageAdd($extra = null, $placeholders = null) {
301
		$link = CMSPageAddController::singleton()->Link();
302
		$this->extend('updateLinkPageAdd', $link);
303
304
		if($extra) {
305
			$link = Controller::join_links ($link, $extra);
306
		}
307
308
		if($placeholders) {
309
			$link .= (strpos($link, '?') === false ? "?$placeholders" : "&amp;$placeholders");
310
		}
311
312
		return $link;
313
	}
314
315
	/**
316
	 * @return string
317
	 */
318
	public function LinkPreview() {
319
		$record = $this->getRecord($this->currentPageID());
320
		$baseLink = Director::absoluteBaseURL();
321
		if ($record && $record instanceof Page) {
0 ignored issues
show
Bug introduced by
The class Page does not exist. Is this class maybe located in a folder that is not analyzed, or in a newer version of your dependencies than listed in your composer.lock/composer.json?
Loading history...
322
			// if we are an external redirector don't show a link
323
			if ($record instanceof RedirectorPage && $record->RedirectionType == 'External') {
324
				$baseLink = false;
325
			}
326
			else {
327
				$baseLink = $record->Link('?stage=Stage');
328
			}
329
		}
330
		return $baseLink;
331
	}
332
333
	/**
334
	 * Return the entire site tree as a nested set of ULs
335
	 */
336
	public function SiteTreeAsUL() {
337
		// Pre-cache sitetree version numbers for querying efficiency
338
		Versioned::prepopulate_versionnumber_cache("SilverStripe\\CMS\\Model\\SiteTree", "Stage");
339
		Versioned::prepopulate_versionnumber_cache("SilverStripe\\CMS\\Model\\SiteTree", "Live");
340
		$html = $this->getSiteTreeFor($this->stat('tree_class'));
341
342
		$this->extend('updateSiteTreeAsUL', $html);
343
344
		return $html;
345
	}
346
347
	/**
348
	 * @return boolean
349
	 */
350
	public function TreeIsFiltered() {
351
		$query = $this->getRequest()->getVar('q');
352
353
		if (!$query || (count($query) === 1 && isset($query['FilterClass']) && $query['FilterClass'] === 'SilverStripe\\CMS\\Controllers\\CMSSiteTreeFilter_Search')) {
354
			return false;
355
		}
356
357
		return true;
358
	}
359
360
	public function ExtraTreeTools() {
361
		$html = '';
362
		$this->extend('updateExtraTreeTools', $html);
363
		return $html;
364
	}
365
366
	/**
367
	 * Returns a Form for page searching for use in templates.
368
	 *
369
	 * Can be modified from a decorator by a 'updateSearchForm' method
370
	 *
371
	 * @return Form
372
	 */
373
	public function SearchForm() {
374
		// Create the fields
375
		$content = new TextField('q[Term]', _t('CMSSearch.FILTERLABELTEXT', 'Search'));
376
		$dateFrom = new DateField(
377
			'q[LastEditedFrom]',
378
			_t('CMSSearch.FILTERDATEFROM', 'From')
379
		);
380
		$dateFrom->setConfig('showcalendar', true);
381
		$dateTo = new DateField(
382
			'q[LastEditedTo]',
383
			_t('CMSSearch.FILTERDATETO', 'To')
384
		);
385
		$dateTo->setConfig('showcalendar', true);
386
		$pageFilter = new DropdownField(
387
			'q[FilterClass]',
388
			_t('CMSMain.PAGES', 'Page status'),
389
			CMSSiteTreeFilter::get_all_filters()
390
		);
391
		$pageClasses = new DropdownField(
392
			'q[ClassName]',
393
			_t('CMSMain.PAGETYPEOPT', 'Page type', 'Dropdown for limiting search to a page type'),
394
			$this->getPageTypes()
395
		);
396
		$pageClasses->setEmptyString(_t('CMSMain.PAGETYPEANYOPT','Any'));
397
398
		// Group the Datefields
399
		$dateGroup = new FieldGroup(
400
			$dateFrom,
401
			$dateTo
402
		);
403
		$dateGroup->setTitle('Last Edited', _t('CMSSearch.PAGEFILTERDATEHEADING', 'Last edited'));
0 ignored issues
show
Unused Code introduced by
The call to FieldGroup::setTitle() has too many arguments starting with _t('CMSSearch.PAGEFILTER...EADING', 'Last edited').

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
404
405
		// Create the Field list
406
		$fields = new FieldList(
407
			$content,
408
			$pageFilter,
409
			$pageClasses,
410
			$dateGroup
411
		);
412
413
		// Create the Search and Reset action
414
		$actions = new FieldList(
415
			FormAction::create('doSearch',  _t('CMSMain_left_ss.APPLY_FILTER', 'Search'))
416
				->addExtraClass('ss-ui-action-constructive'),
417
			ResetFormAction::create('clear', _t('CMSMain_left_ss.CLEAR_FILTER', 'Clear'))
418
		);
419
420
		// Use <button> to allow full jQuery UI styling on the all of the Actions
421
		foreach($actions->dataFields() as $action) {
422
			/** @var FormAction $action */
423
			$action->setUseButtonTag(true);
424
		}
425
426
		// Create the form
427
		/** @skipUpgrade */
428
		$form = Form::create($this, 'SearchForm', $fields, $actions)
429
			->addExtraClass('cms-search-form')
430
			->setFormMethod('GET')
431
			->setFormAction($this->Link())
432
			->disableSecurityToken()
433
			->unsetValidator();
434
435
		// Load the form with previously sent search data
436
		$form->loadDataFrom($this->getRequest()->getVars());
437
438
		// Allow decorators to modify the form
439
		$this->extend('updateSearchForm', $form);
440
441
		return $form;
442
	}
443
444
	/**
445
	 * Returns a sorted array suitable for a dropdown with pagetypes and their translated name
446
	 *
447
	 * @return array
448
	 */
449
	protected function getPageTypes() {
450
		$pageTypes = array();
451
		foreach(SiteTree::page_type_classes() as $pageTypeClass) {
452
			$pageTypes[$pageTypeClass] = SiteTree::singleton($pageTypeClass)->i18n_singular_name();
453
		}
454
		asort($pageTypes);
455
		return $pageTypes;
456
	}
457
458
	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...
459
		return $this->getsubtree($this->getRequest());
460
	}
461
462
	/**
463
	 * @param bool $unlinked
464
	 * @return ArrayList
465
	 */
466
	public function Breadcrumbs($unlinked = false) {
467
		$items = parent::Breadcrumbs($unlinked);
468
469
		if($items->count() > 1) {
470
			// Specific to the SiteTree admin section, we never show the cms section and current
471
			// page in the same breadcrumbs block.
472
			$items->shift();
473
		}
474
475
		return $items;
476
	}
477
478
	/**
479
	 * Create serialized JSON string with site tree hints data to be injected into
480
	 * 'data-hints' attribute of root node of jsTree.
481
	 *
482
	 * @return string Serialized JSON
483
	 */
484
	public function SiteTreeHints() {
485
		$json = '';
0 ignored issues
show
Unused Code introduced by
$json 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...
486
		$classes = SiteTree::page_type_classes();
487
488
		$cacheCanCreate = array();
489
		foreach($classes as $class) $cacheCanCreate[$class] = singleton($class)->canCreate();
490
491
		// Generate basic cache key. Too complex to encompass all variations
492
		$cache = SS_Cache::factory('CMSMain_SiteTreeHints');
493
		$cacheKey = md5(implode('_', array(Member::currentUserID(), implode(',', $cacheCanCreate), implode(',', $classes))));
494
		if($this->getRequest()->getVar('flush')) $cache->clean(Zend_Cache::CLEANING_MODE_ALL);
495
		$json = $cache->load($cacheKey);
496
	 	if(!$json) {
497
			$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...
498
			$def['Root']['disallowedChildren'] = array();
499
500
			// Contains all possible classes to support UI controls listing them all,
501
			// such as the "add page here" context menu.
502
			$def['All'] = array();
503
504
			// Identify disallows and set globals
505
			foreach($classes as $class) {
506
				$obj = singleton($class);
507
				if($obj instanceof HiddenClass) continue;
508
509
				// Name item
510
				$def['All'][$class] = array(
511
					'title' => $obj->i18n_singular_name()
512
				);
513
514
				// Check if can be created at the root
515
				$needsPerm = $obj->stat('need_permission');
516
				if(
517
					!$obj->stat('can_be_root')
518
					|| (!array_key_exists($class, $cacheCanCreate) || !$cacheCanCreate[$class])
519
					|| ($needsPerm && !$this->can($needsPerm))
520
				) {
521
					$def['Root']['disallowedChildren'][] = $class;
522
				}
523
524
				// Hint data specific to the class
525
				$def[$class] = array();
526
527
				$defaultChild = $obj->defaultChild();
528
				if($defaultChild !== 'Page' && $defaultChild !== null) {
529
					$def[$class]['defaultChild'] = $defaultChild;
530
				}
531
532
				$defaultParent = $obj->defaultParent();
533
				if ($defaultParent !== 1 && $defaultParent !== null) {
534
					$def[$class]['defaultParent'] = $defaultParent;
535
				}
536
			}
537
538
			$this->extend('updateSiteTreeHints', $def);
539
540
			$json = Convert::raw2json($def);
541
			$cache->save($json, $cacheKey);
542
		}
543
		return $json;
544
	}
545
546
	/**
547
	 * Populates an array of classes in the CMS
548
	 * which allows the user to change the page type.
549
	 *
550
	 * @return SS_List
551
	 */
552
	public function PageTypes() {
553
		$classes = SiteTree::page_type_classes();
554
555
		$result = new ArrayList();
556
557
		foreach($classes as $class) {
558
			$instance = singleton($class);
559
560
			if($instance instanceof HiddenClass) {
561
				continue;
562
			}
563
564
			// skip this type if it is restricted
565
			if($instance->stat('need_permission') && !$this->can(singleton($class)->stat('need_permission'))) {
566
				continue;
567
			}
568
569
			$addAction = $instance->i18n_singular_name();
570
571
			// Get description (convert 'Page' to 'SiteTree' for correct localization lookups)
572
			$i18nClass = ($class == 'Page') ? 'SilverStripe\\CMS\\Model\\SiteTree' : $class;
573
			$description = _t($i18nClass . '.DESCRIPTION');
574
575
			if(!$description) {
576
				$description = $instance->uninherited('description');
577
			}
578
579
			if($class == 'Page' && !$description) {
580
				$description = SiteTree::singleton()->uninherited('description');
581
			}
582
583
			$result->push(new ArrayData(array(
584
				'ClassName' => $class,
585
				'AddAction' => $addAction,
586
				'Description' => $description,
587
				// TODO Sprite support
588
				'IconURL' => $instance->stat('icon'),
589
				'Title' => singleton($class)->i18n_singular_name(),
590
			)));
591
		}
592
593
		$result = $result->sort('AddAction');
594
595
		return $result;
596
	}
597
598
	/**
599
	 * Get a database record to be managed by the CMS.
600
	 *
601
	 * @param int $id Record ID
602
	 * @param int $versionID optional Version id of the given record
603
	 * @return SiteTree
604
	 */
605
 	public function getRecord($id, $versionID = null) {
606
		$treeClass = $this->stat('tree_class');
607
608
		if($id instanceof $treeClass) {
609
			return $id;
610
		}
611
		else if($id && is_numeric($id)) {
612
			$currentStage = Versioned::get_reading_mode();
613
614
			if($this->getRequest()->getVar('Version')) {
615
				$versionID = (int) $this->getRequest()->getVar('Version');
616
			}
617
618
			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...
619
				$record = Versioned::get_version($treeClass, $id, $versionID);
620
			} else {
621
				$record = DataObject::get_by_id($treeClass, $id);
622
			}
623
624
			// Then, try getting a record from the live site
625
			if(!$record) {
626
				// $record = Versioned::get_one_by_stage($treeClass, "Live", "\"$treeClass\".\"ID\" = $id");
627
				Versioned::set_stage(Versioned::LIVE);
628
				singleton($treeClass)->flushCache();
629
630
				$record = DataObject::get_by_id($treeClass, $id);
631
			}
632
633
			// Then, try getting a deleted record
634
			if(!$record) {
635
				$record = Versioned::get_latest_version($treeClass, $id);
636
			}
637
638
			// Don't open a page from a different locale
639
			/** The record's Locale is saved in database in 2.4, and not related with Session,
640
			 *  we should not check their locale matches the Translatable::get_current_locale,
641
			 * 	here as long as we all the HTTPRequest is init with right locale.
642
			 *	This bit breaks the all FileIFrameField functions if the field is used in CMS
643
			 *  and its relevent ajax calles, like loading the tree dropdown for TreeSelectorField.
644
			 */
645
			/* if($record && SiteTree::has_extension('Translatable') && $record->Locale && $record->Locale != Translatable::get_current_locale()) {
646
				$record = null;
647
			}*/
648
649
			// Set the reading mode back to what it was.
650
			Versioned::set_reading_mode($currentStage);
651
652
			return $record;
653
654
		} else if(substr($id,0,3) == 'new') {
655
			return $this->getNewItem($id);
656
		}
657
	}
658
659
	/**
660
	 * @param int $id
661
	 * @param FieldList $fields
662
	 * @return Form
663
	 */
664
	public function getEditForm($id = null, $fields = null) {
665
		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...
666
		$form = parent::getEditForm($id, $fields);
667
668
		// TODO Duplicate record fetching (see parent implementation)
669
		$record = $this->getRecord($id);
670
		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 SS_HTTPResponse|null adds the type SS_HTTPResponse to the return on line 670 which is incompatible with the return type documented by SilverStripe\CMS\Controllers\CMSMain::getEditForm of type Form.
Loading history...
671
672
		if(!$fields) $fields = $form->Fields();
673
		$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...
674
675
		if($record) {
676
			$deletedFromStage = $record->getIsDeletedFromStage();
677
678
			$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...
679
			// Necessary for different subsites
680
			$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...
681
			$fields->push($liveLinkField = new HiddenField("LiveLink"));
682
			$fields->push($stageLinkField = new HiddenField("StageLink"));
683
			$fields->push(new HiddenField("TreeTitle", false, $record->TreeTitle));
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...
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...
684
685
			if($record->ID && is_numeric( $record->ID ) ) {
686
				$liveLink = $record->getAbsoluteLiveLink();
687
				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...
688
				if(!$deletedFromStage) {
689
					$stageLink = Controller::join_links($record->AbsoluteLink(), '?stage=Stage');
690
					if($stageLink) $stageLinkField->setValue($stageLink);
691
				}
692
			}
693
694
			// Added in-line to the form, but plucked into different view by LeftAndMain.Preview.js upon load
695
			/** @skipUpgrade */
696
			if($record instanceof CMSPreviewable && !$fields->fieldByName('SilverStripeNavigator')) {
697
				$navField = new LiteralField('SilverStripeNavigator', $this->getSilverStripeNavigator());
698
				$navField->setAllowHTML(true);
699
				$fields->push($navField);
700
			}
701
702
			// getAllCMSActions can be used to completely redefine the action list
703
			if($record->hasMethod('getAllCMSActions')) {
704
				$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...
705
			} else {
706
				$actions = $record->getCMSActions();
707
708
				// Find and remove action menus that have no actions.
709
				if ($actions && $actions->Count()) {
710
					$tabset = $actions->fieldByName('ActionMenus');
711
					if ($tabset) {
712
						foreach ($tabset->getChildren() as $tab) {
713
							if (!$tab->getChildren()->count()) {
714
								$tabset->removeByName($tab->getName());
715
							}
716
						}
717
					}
718
				}
719
			}
720
721
			// Use <button> to allow full jQuery UI styling
722
			$actionsFlattened = $actions->dataFields();
723
			if($actionsFlattened) foreach($actionsFlattened as $action) $action->setUseButtonTag(true);
724
725
			if($record->hasMethod('getCMSValidator')) {
726
				$validator = $record->getCMSValidator();
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...
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...
727
			} else {
728
				$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...
729
			}
730
731
			// TODO Can't merge $FormAttributes in template at the moment
732
			$form->addExtraClass('center ' . $this->BaseCSSClasses());
733
			// Set validation exemptions for specific actions
734
			$form->setValidationExemptActions(array('restore', 'revert', 'deletefromlive', 'delete', 'unpublish', 'rollback', 'doRollback'));
735
736
			// Announce the capability so the frontend can decide whether to allow preview or not.
737
			if(in_array('CMSPreviewable', class_implements($record))) {
738
				$form->addExtraClass('cms-previewable');
739
			}
740
741
			if(!$record->canEdit() || $deletedFromStage) {
742
				$readonlyFields = $form->Fields()->makeReadonly();
743
				$form->setFields($readonlyFields);
744
			}
745
746
			$form->Fields()->setForm($form);
747
748
			$this->extend('updateEditForm', $form);
749
			return $form;
750
		} else if($id) {
751
			$form = Form::create( $this, "EditForm", new FieldList(
752
				new LabelField('PageDoesntExistLabel',_t('CMSMain.PAGENOTEXISTS',"This page doesn't exist"))), new FieldList()
753
			)->setHTMLID('Form_EditForm');
754
			return $form;
755
		}
756
	}
757
758
	/**
759
	 * @param SS_HTTPRequest $request
760
	 * @return string HTML
761
	 */
762
	public function treeview($request) {
0 ignored issues
show
Unused Code introduced by
The parameter $request 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...
763
		return $this->renderWith($this->getTemplatesWithSuffix('_TreeView'));
764
	}
765
766
	/**
767
	 * @param SS_HTTPRequest $request
768
	 * @return string HTML
769
	 */
770
	public function listview($request) {
0 ignored issues
show
Unused Code introduced by
The parameter $request 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...
771
		return $this->renderWith($this->getTemplatesWithSuffix('_ListView'));
772
	}
773
774
	/**
775
	 * Callback to request the list of page types allowed under a given page instance.
776
	 * Provides a slower but more precise response over SiteTreeHints
777
	 *
778
	 * @param SS_HTTPRequest $request
779
	 * @return SS_HTTPResponse
780
	 */
781
	public function childfilter($request) {
782
		// Check valid parent specified
783
		$parentID = $request->requestVar('ParentID');
784
		$parent = SiteTree::get()->byID($parentID);
785
		if(!$parent || !$parent->exists()) return $this->httpError(404);
786
787
		// Build hints specific to this class
788
		// Identify disallows and set globals
789
		$classes = SiteTree::page_type_classes();
790
		$disallowedChildren = array();
791
		foreach($classes as $class) {
792
			$obj = singleton($class);
793
			if($obj instanceof HiddenClass) continue;
794
795
			if(!$obj->canCreate(null, array('Parent' => $parent))) {
796
				$disallowedChildren[] = $class;
797
			}
798
		}
799
800
		$this->extend('updateChildFilter', $disallowedChildren, $parentID);
801
		return $this
802
			->getResponse()
803
			->addHeader('Content-Type', 'application/json; charset=utf-8')
804
			->setBody(Convert::raw2json($disallowedChildren));
805
	}
806
807
	/**
808
	 * Safely reconstruct a selected filter from a given set of query parameters
809
	 *
810
	 * @param array $params Query parameters to use
811
	 * @return CMSSiteTreeFilter The filter class, or null if none present
812
	 * @throws InvalidArgumentException if invalid filter class is passed.
813
	 */
814
	protected function getQueryFilter($params) {
815
		if(empty($params['FilterClass'])) return null;
816
		$filterClass = $params['FilterClass'];
817
		if(!is_subclass_of($filterClass, 'SilverStripe\\CMS\\Controllers\\CMSSiteTreeFilter')) {
818
			throw new InvalidArgumentException("Invalid filter class passed: {$filterClass}");
819
		}
820
		return $filterClass::create($params);
821
	}
822
823
	/**
824
	 * Returns the pages meet a certain criteria as {@see CMSSiteTreeFilter} or the subpages of a parent page
825
	 * defaulting to no filter and show all pages in first level.
826
	 * Doubles as search results, if any search parameters are set through {@link SearchForm()}.
827
	 *
828
	 * @param array $params Search filter criteria
829
	 * @param int $parentID Optional parent node to filter on (can't be combined with other search criteria)
830
	 * @return SS_List
831
	 * @throws InvalidArgumentException if invalid filter class is passed.
832
	 */
833
	public function getList($params = array(), $parentID = 0) {
834
		if($filter = $this->getQueryFilter($params)) {
835
			return $filter->getFilteredPages();
836
		} else {
837
			$list = DataList::create($this->stat('tree_class'));
838
			$parentID = is_numeric($parentID) ? $parentID : 0;
839
			return $list->filter("ParentID", $parentID);
840
		}
841
	}
842
843
	public function ListViewForm() {
844
		$params = $this->getRequest()->requestVar('q');
845
		$list = $this->getList($params, $parentID = $this->getRequest()->requestVar('ParentID'));
846
		$gridFieldConfig = GridFieldConfig::create()->addComponents(
847
			new GridFieldSortableHeader(),
848
			new GridFieldDataColumns(),
849
			new GridFieldPaginator(self::config()->page_length)
850
		);
851
		if($parentID){
852
			$gridFieldConfig->addComponent(
853
				GridFieldLevelup::create($parentID)
854
					->setLinkSpec('?ParentID=%d&view=list')
855
					->setAttributes(array('data-pjax' => 'ListViewForm,Breadcrumbs'))
856
			);
857
		}
858
		$gridField = new GridField('Page','Pages', $list, $gridFieldConfig);
859
		$columns = $gridField->getConfig()->getComponentByType('GridFieldDataColumns');
860
861
		// Don't allow navigating into children nodes on filtered lists
862
		$fields = array(
863
			'getTreeTitle' => _t('SiteTree.PAGETITLE', 'Page Title'),
864
			'singular_name' => _t('SiteTree.PAGETYPE'),
865
			'LastEdited' => _t('SiteTree.LASTUPDATED', 'Last Updated'),
866
		);
867
		$gridField->getConfig()->getComponentByType('GridFieldSortableHeader')->setFieldSorting(array('getTreeTitle' => 'Title'));
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface GridFieldComponent as the method setFieldSorting() does only exist in the following implementations of said interface: GridFieldSortableHeader.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
868
		$gridField->getState()->ParentID = $parentID;
869
870
		if(!$params) {
871
			$fields = array_merge(array('listChildrenLink' => ''), $fields);
872
		}
873
874
		$columns->setDisplayFields($fields);
875
		$columns->setFieldCasting(array(
876
			'Created' => 'DBDatetime->Ago',
877
			'LastEdited' => 'DBDatetime->FormatFromSettings',
878
			'getTreeTitle' => 'HTMLFragment'
879
		));
880
881
		$controller = $this;
882
		$columns->setFieldFormatting(array(
883
			'listChildrenLink' => function($value, &$item) use($controller) {
884
				$num = $item ? $item->numChildren() : null;
885
				if($num) {
886
					return sprintf(
887
						'<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>',
888
						Controller::join_links(
889
							$controller->Link(),
890
							sprintf("?ParentID=%d&view=list", (int)$item->ID)
891
						),
892
						$num
893
					);
894
				}
895
			},
896
			'getTreeTitle' => function($value, &$item) use($controller) {
897
				return sprintf(
898
					'<a class="action-detail" href="%s">%s</a>',
899
					Controller::join_links(
900
						CMSPageEditController::singleton()->Link('show'),
901
						(int)$item->ID
902
					),
903
					$item->TreeTitle // returns HTML, does its own escaping
904
				);
905
			}
906
		));
907
908
		$negotiator = $this->getResponseNegotiator();
909
		$listview = Form::create(
910
			$this,
911
			'ListViewForm',
912
			new FieldList($gridField),
913
			new FieldList()
914
		)->setHTMLID('Form_ListViewForm');
915
		$listview->setAttribute('data-pjax-fragment', 'ListViewForm');
916 View Code Duplication
		$listview->setValidationResponseCallback(function() use ($negotiator, $listview) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
917
			$request = $this->getRequest();
918
			if($request->isAjax() && $negotiator) {
919
				$listview->setupFormErrors();
920
				$result = $listview->forTemplate();
921
922
				return $negotiator->respond($request, array(
923
					'CurrentForm' => function() use($result) {
924
						return $result;
925
					}
926
				));
927
			}
928
		});
929
930
		$this->extend('updateListView', $listview);
931
932
		$listview->disableSecurityToken();
933
		return $listview;
934
	}
935
936
	public function currentPageID() {
937
		$id = parent::currentPageID();
938
939
		$this->extend('updateCurrentPageID', $id);
940
941
		return $id;
942
	}
943
944
	//------------------------------------------------------------------------------------------//
945
	// Data saving handlers
946
947
	/**
948
	 * Save and Publish page handler
949
	 *
950
	 * @param array $data
951
	 * @param Form $form
952
	 * @return SS_HTTPResponse
953
	 * @throws SS_HTTPResponse_Exception
954
	 */
955
	public function save($data, $form) {
956
		$className = $this->stat('tree_class');
957
958
		// Existing or new record?
959
		$id = $data['ID'];
960
		if(substr($id,0,3) != 'new') {
961
			/** @var SiteTree $record */
962
			$record = DataObject::get_by_id($className, $id);
963
			// Check edit permissions
964
			if($record && !$record->canEdit()) {
965
				return Security::permissionFailure($this);
966
			}
967
			if(!$record || !$record->ID) {
968
				throw new SS_HTTPResponse_Exception("Bad record ID #$id", 404);
969
			}
970
		} else {
971
			if(!$className::singleton()->canCreate()) {
972
				return Security::permissionFailure($this);
973
			}
974
			$record = $this->getNewItem($id, false);
975
		}
976
977
		// Check publishing permissions
978
		$doPublish = !empty($data['publish']);
979
		if($record && $doPublish && !$record->canPublish()) {
980
			return Security::permissionFailure($this);
981
		}
982
983
		// TODO Coupling to SiteTree
984
		$record->HasBrokenLink = 0;
985
		$record->HasBrokenFile = 0;
986
987
		if (!$record->ObsoleteClassName) {
988
			$record->writeWithoutVersion();
989
		}
990
991
		// Update the class instance if necessary
992
		if(isset($data['ClassName']) && $data['ClassName'] != $record->ClassName) {
993
			$newClassName = $record->ClassName;
994
			// The records originally saved attribute was overwritten by $form->saveInto($record) before.
995
			// This is necessary for newClassInstance() to work as expected, and trigger change detection
996
			// on the ClassName attribute
997
			$record->setClassName($data['ClassName']);
998
			// Replace $record with a new instance
999
			$record = $record->newClassInstance($newClassName);
1000
		}
1001
1002
		// save form data into record
1003
		$form->saveInto($record);
1004
		$record->write();
1005
1006
		// If the 'Save & Publish' button was clicked, also publish the page
1007
		if ($doPublish) {
1008
			$record->publishRecursive();
1009
			$message = _t(
1010
				'CMSMain.PUBLISHED',
1011
				"Published '{title}' successfully.",
1012
				['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...
1013
			);
1014
		} else {
1015
			$message = _t(
1016
				'CMSMain.SAVED',
1017
				"Saved '{title}' successfully.",
1018
				['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...
1019
			);
1020
		}
1021
1022
		$this->getResponse()->addHeader('X-Status', rawurlencode($message));
1023
		return $this->getResponseNegotiator()->respond($this->getRequest());
1024
	}
1025
1026
	/**
1027
	 * @uses LeftAndMainExtension->augmentNewSiteTreeItem()
1028
	 */
1029
	public function getNewItem($id, $setID = true) {
1030
		$parentClass = $this->stat('tree_class');
1031
		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...
1032
1033
		if(!is_subclass_of($className, $parentClass) && strcasecmp($className, $parentClass) != 0) {
0 ignored issues
show
Bug introduced by
Due to PHP Bug #53727, is_subclass_of might return inconsistent results on some PHP versions if $parentClass can be an interface. If so, you could instead use ReflectionClass::implementsInterface.
Loading history...
1034
			$response = Security::permissionFailure($this);
1035
			if (!$response) {
1036
				$response = $this->getResponse();
1037
			}
1038
			throw new SS_HTTPResponse_Exception($response);
1039
		}
1040
1041
		$newItem = new $className();
1042
1043
		if( !$suffix ) {
1044
			$sessionTag = "NewItems." . $parentID . "." . $className;
1045
			if(Session::get($sessionTag)) {
1046
				$suffix = '-' . Session::get($sessionTag);
1047
				Session::set($sessionTag, Session::get($sessionTag) + 1);
1048
			}
1049
			else
1050
				Session::set($sessionTag, 1);
1051
1052
				$id = $id . $suffix;
1053
		}
1054
1055
		$newItem->Title = _t(
1056
			'CMSMain.NEWPAGE',
1057
			"New {pagetype}",'followed by a page type title',
1058
			array('pagetype' => singleton($className)->i18n_singular_name())
0 ignored issues
show
Documentation introduced by
array('pagetype' => sing...->i18n_singular_name()) is of type array<string,?,{"pagetype":"?"}>, 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...
1059
		);
1060
		$newItem->ClassName = $className;
1061
		$newItem->ParentID = $parentID;
1062
1063
		// DataObject::fieldExists only checks the current class, not the hierarchy
1064
		// This allows the CMS to set the correct sort value
1065
		if($newItem->castingHelper('Sort')) {
1066
			$newItem->Sort = DB::prepared_query('SELECT MAX("Sort") FROM "SiteTree" WHERE "ParentID" = ?', array($parentID))->value() + 1;
1067
		}
1068
1069
		if($setID) $newItem->ID = $id;
1070
1071
		# Some modules like subsites add extra fields that need to be set when the new item is created
1072
		$this->extend('augmentNewSiteTreeItem', $newItem);
1073
1074
		return $newItem;
1075
	}
1076
1077
	/**
1078
	 * Actually perform the publication step
1079
	 *
1080
	 * @param Versioned|DataObject $record
1081
	 * @return mixed
1082
	 */
1083
	public function performPublish($record) {
1084
		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...
1085
			return Security::permissionFailure($this);
1086
		}
1087
1088
		$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...
1089
	}
1090
1091
	/**
1092
	 * Reverts a page by publishing it to live.
1093
	 * Use {@link restorepage()} if you want to restore a page
1094
	 * which was deleted from draft without publishing.
1095
	 *
1096
	 * @uses SiteTree->doRevertToLive()
1097
	 *
1098
	 * @param array $data
1099
	 * @param Form $form
1100
	 * @return SS_HTTPResponse
1101
	 * @throws SS_HTTPResponse_Exception
1102
	 */
1103
	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...
1104
		if(!isset($data['ID'])) {
1105
			throw new SS_HTTPResponse_Exception("Please pass an ID in the form content", 400);
1106
		}
1107
1108
		$id = (int) $data['ID'];
1109
		$restoredPage = Versioned::get_latest_version("SilverStripe\\CMS\\Model\\SiteTree", $id);
1110
		if(!$restoredPage) {
1111
			throw new SS_HTTPResponse_Exception("SiteTree #$id not found", 400);
1112
		}
1113
1114
		/** @var SiteTree $record */
1115
		$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...
1116
			'"SiteTree_Live"."ID"' => $id
1117
		));
1118
1119
		// a user can restore a page without publication rights, as it just adds a new draft state
1120
		// (this action should just be available when page has been "deleted from draft")
1121
		if($record && !$record->canEdit()) {
1122
			return Security::permissionFailure($this);
1123
		}
1124
		if(!$record || !$record->ID) {
1125
			throw new SS_HTTPResponse_Exception("Bad record ID #$id", 404);
1126
		}
1127
1128
		$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...
1129
1130
		$this->getResponse()->addHeader(
1131
			'X-Status',
1132
			rawurlencode(_t(
1133
				'CMSMain.RESTORED',
1134
				"Restored '{title}' successfully",
1135
				'Param %s is a title',
1136
				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...
1137
			))
1138
		);
1139
1140
		return $this->getResponseNegotiator()->respond($this->getRequest());
1141
	}
1142
1143
	/**
1144
	 * Delete the current page from draft stage.
1145
	 *
1146
	 * @see deletefromlive()
1147
	 *
1148
	 * @param array $data
1149
	 * @param Form $form
1150
	 * @return SS_HTTPResponse
1151
	 * @throws SS_HTTPResponse_Exception
1152
	 */
1153 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...
1154
		$id = $data['ID'];
1155
		$record = SiteTree::get()->byID($id);
1156
		if($record && !$record->canDelete()) {
1157
			return Security::permissionFailure();
1158
		}
1159
		if(!$record || !$record->ID) {
1160
			throw new SS_HTTPResponse_Exception("Bad record ID #$id", 404);
1161
		}
1162
1163
		// Delete record
1164
		$record->delete();
1165
1166
		$this->getResponse()->addHeader(
1167
			'X-Status',
1168
			rawurlencode(sprintf(_t('CMSMain.REMOVEDPAGEFROMDRAFT',"Removed '%s' from the draft site"), $record->Title))
1169
		);
1170
1171
		// Even if the record has been deleted from stage and live, it can be viewed in "archive mode"
1172
		return $this->getResponseNegotiator()->respond($this->getRequest());
1173
	}
1174
1175
	/**
1176
	 * Delete this page from both live and stage
1177
	 *
1178
	 * @param array $data
1179
	 * @param Form $form
1180
	 * @return SS_HTTPResponse
1181
	 * @throws SS_HTTPResponse_Exception
1182
	 */
1183 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...
1184
		$id = $data['ID'];
1185
		/** @var SiteTree $record */
1186
		$record = SiteTree::get()->byID($id);
1187
		if(!$record || !$record->exists()) {
1188
			throw new SS_HTTPResponse_Exception("Bad record ID #$id", 404);
1189
		}
1190
		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...
1191
			return Security::permissionFailure();
1192
		}
1193
1194
		// Archive record
1195
		$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...
1196
1197
		$this->getResponse()->addHeader(
1198
			'X-Status',
1199
			rawurlencode(sprintf(_t('CMSMain.ARCHIVEDPAGE',"Archived page '%s'"), $record->Title))
1200
		);
1201
1202
		// Even if the record has been deleted from stage and live, it can be viewed in "archive mode"
1203
		return $this->getResponseNegotiator()->respond($this->getRequest());
1204
	}
1205
1206
	public function publish($data, $form) {
1207
		$data['publish'] = '1';
1208
1209
		return $this->save($data, $form);
1210
	}
1211
1212
	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...
1213
		$className = $this->stat('tree_class');
1214
		/** @var SiteTree $record */
1215
		$record = DataObject::get_by_id($className, $data['ID']);
1216
1217
		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...
1218
			return Security::permissionFailure($this);
1219
		}
1220 View Code Duplication
		if(!$record || !$record->ID) {
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...
1221
			throw new SS_HTTPResponse_Exception("Bad record ID #" . (int)$data['ID'], 404);
1222
		}
1223
1224
		$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...
1225
1226
		$this->getResponse()->addHeader(
1227
			'X-Status',
1228
			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...
1229
		);
1230
1231
		return $this->getResponseNegotiator()->respond($this->getRequest());
1232
	}
1233
1234
	/**
1235
	 * @return array
1236
	 */
1237
	public function rollback() {
1238
		return $this->doRollback(array(
1239
			'ID' => $this->currentPageID(),
1240
			'Version' => $this->getRequest()->param('VersionID')
1241
		), null);
0 ignored issues
show
Documentation introduced by
null is of type null, but the function expects a object<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...
1242
	}
1243
1244
	/**
1245
	 * Rolls a site back to a given version ID
1246
	 *
1247
	 * @param array $data
1248
	 * @param Form $form
1249
	 * @return SS_HTTPResponse
1250
	 */
1251
	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...
1252
		$this->extend('onBeforeRollback', $data['ID']);
1253
1254
		$id = (isset($data['ID'])) ? (int) $data['ID'] : null;
1255
		$version = (isset($data['Version'])) ? (int) $data['Version'] : null;
1256
1257
		/** @var DataObject|Versioned $record */
1258
		$record = DataObject::get_by_id($this->stat('tree_class'), $id);
1259
		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...
1260
			return Security::permissionFailure($this);
1261
		}
1262
1263
		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...
1264
			$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...
1265
			$message = _t(
1266
				'CMSMain.ROLLEDBACKVERSIONv2',
1267
				"Rolled back to version #%d.",
1268
				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...
1269
			);
1270
		} else {
1271
			$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...
1272
			$message = _t(
1273
				'CMSMain.ROLLEDBACKPUBv2',"Rolled back to published version."
1274
			);
1275
		}
1276
1277
		$this->getResponse()->addHeader('X-Status', rawurlencode($message));
1278
1279
		// Can be used in different contexts: In normal page edit view, in which case the redirect won't have any effect.
1280
		// Or in history view, in which case a revert causes the CMS to re-load the edit view.
1281
		// The X-Pjax header forces a "full" content refresh on redirect.
1282
		$url = Controller::join_links(CMSPageEditController::singleton()->Link('show'), $record->ID);
1283
		$this->getResponse()->addHeader('X-ControllerURL', $url);
1284
		$this->getRequest()->addHeader('X-Pjax', 'Content');
1285
		$this->getResponse()->addHeader('X-Pjax', 'Content');
1286
1287
		return $this->getResponseNegotiator()->respond($this->getRequest());
1288
	}
1289
1290
	/**
1291
	 * Action handler for adding pages to a campaign
1292
	 *
1293
	 * @param array $data
1294
	 * @param Form $form
1295
	 * @return DBHTMLText|SS_HTTPResponse
1296
	 */
1297
	public function addtocampaign($data, $form) {
1298
		$handler = AddToCampaignHandler::create($form, $data);
1299
		return $handler->handle();
1300
	}
1301
1302
	/**
1303
	 * Batch Actions Handler
1304
	 */
1305
	public function batchactions() {
1306
		return new CMSBatchActionHandler($this, 'batchactions');
1307
	}
1308
1309
	public function BatchActionParameters() {
1310
		$batchActions = CMSBatchActionHandler::config()->batch_actions;
1311
1312
		$forms = array();
1313
		foreach($batchActions as $urlSegment => $batchAction) {
1314
			$SNG_action = singleton($batchAction);
1315
			if ($SNG_action->canView() && $fieldset = $SNG_action->getParameterFields()) {
1316
				$formHtml = '';
1317
				foreach($fieldset as $field) {
1318
					$formHtml .= $field->Field();
1319
				}
1320
				$forms[$urlSegment] = $formHtml;
1321
			}
1322
		}
1323
		$pageHtml = '';
1324
		foreach($forms as $urlSegment => $html) {
1325
			$pageHtml .= "<div class=\"params\" id=\"BatchActionParameters_$urlSegment\">$html</div>\n\n";
1326
		}
1327
		return new LiteralField("BatchActionParameters", '<div id="BatchActionParameters" style="display:none">'.$pageHtml.'</div>');
1328
	}
1329
	/**
1330
	 * Returns a list of batch actions
1331
	 */
1332
	public function BatchActionList() {
1333
		return $this->batchactions()->batchActionList();
1334
	}
1335
1336
	public function publishall($request) {
1337
		if(!Permission::check('ADMIN')) return Security::permissionFailure($this);
1338
1339
		increase_time_limit_to();
1340
		increase_memory_limit_to();
1341
1342
		$response = "";
1343
1344
		if(isset($this->requestParams['confirm'])) {
1345
			// Protect against CSRF on destructive action
1346
			if(!SecurityToken::inst()->checkRequest($request)) return $this->httpError(400);
1347
1348
			$start = 0;
1349
			$pages = SiteTree::get()->limit("$start,30");
1350
			$count = 0;
1351
			while($pages) {
1352
				foreach($pages as $page) {
1353
					if($page && !$page->canPublish()) {
1354
						return Security::permissionFailure($this);
1355
					}
1356
1357
					$page->publishRecursive();
1358
					$page->destroy();
1359
					unset($page);
1360
					$count++;
1361
					$response .= "<li>$count</li>";
1362
				}
1363
				if($pages->count() > 29) {
1364
					$start += 30;
1365
					$pages = SiteTree::get()->limit("$start,30");
1366
				} else {
1367
					break;
1368
				}
1369
			}
1370
			$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...
1371
1372
		} else {
1373
			$token = SecurityToken::inst();
1374
			$fields = new FieldList();
1375
			$token->updateFieldSet($fields);
1376
			$tokenField = $fields->First();
1377
			$tokenHtml = ($tokenField) ? $tokenField->FieldHolder() : '';
1378
			$response .= '<h1>' . _t('CMSMain.PUBALLFUN','"Publish All" functionality') . '</h1>
1379
				<p>' . _t('CMSMain.PUBALLFUN2', 'Pressing this button will do the equivalent of going to every page and pressing "publish".  It\'s
1380
				intended to be used after there have been massive edits of the content, such as when the site was
1381
				first built.') . '</p>
1382
				<form method="post" action="publishall">
1383
					<input type="submit" name="confirm" value="'
1384
					. _t('CMSMain.PUBALLCONFIRM',"Please publish every page in the site, copying content stage to live",'Confirmation button') .'" />'
1385
					. $tokenHtml .
1386
				'</form>';
1387
		}
1388
1389
		return $response;
1390
	}
1391
1392
	/**
1393
	 * Restore a completely deleted page from the SiteTree_versions table.
1394
	 */
1395
	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...
1396
		if(!isset($data['ID']) || !is_numeric($data['ID'])) {
1397
			return new SS_HTTPResponse("Please pass an ID in the form content", 400);
1398
		}
1399
1400
		$id = (int)$data['ID'];
1401
		$restoredPage = Versioned::get_latest_version("SilverStripe\\CMS\\Model\\SiteTree", $id);
1402
		if(!$restoredPage) 	return new SS_HTTPResponse("SiteTree #$id not found", 400);
1403
1404
		$restoredPage = $restoredPage->doRestoreToStage();
1405
1406
		$this->getResponse()->addHeader(
1407
			'X-Status',
1408
			rawurlencode(_t(
1409
				'CMSMain.RESTORED',
1410
				"Restored '{title}' successfully",
1411
				array('title' => $restoredPage->Title)
0 ignored issues
show
Documentation introduced by
array('title' => $restoredPage->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...
1412
			))
1413
		);
1414
1415
		return $this->getResponseNegotiator()->respond($this->getRequest());
1416
	}
1417
1418
	public function duplicate($request) {
1419
		// Protect against CSRF on destructive action
1420
		if(!SecurityToken::inst()->checkRequest($request)) return $this->httpError(400);
1421
1422
		if(($id = $this->urlParams['ID']) && is_numeric($id)) {
1423
			$page = SiteTree::get()->byID($id);
1424 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...
Bug introduced by
The method Parent() does not exist on SilverStripe\ORM\DataObject. Did you maybe mean parentClass()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
1425
				return Security::permissionFailure($this);
1426
			}
1427
			if(!$page || !$page->ID) throw new SS_HTTPResponse_Exception("Bad record ID #$id", 404);
1428
1429
			$newPage = $page->duplicate();
1430
1431
			// ParentID can be hard-set in the URL.  This is useful for pages with multiple parents
1432
			if(isset($_GET['parentID']) && is_numeric($_GET['parentID'])) {
1433
				$newPage->ParentID = $_GET['parentID'];
1434
				$newPage->write();
1435
			}
1436
1437
			$this->getResponse()->addHeader(
1438
				'X-Status',
1439
				rawurlencode(_t(
1440
					'CMSMain.DUPLICATED',
1441
					"Duplicated '{title}' successfully",
1442
					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...
1443
				))
1444
			);
1445
			$url = Controller::join_links(CMSPageEditController::singleton()->Link('show'), $newPage->ID);
1446
			$this->getResponse()->addHeader('X-ControllerURL', $url);
1447
			$this->getRequest()->addHeader('X-Pjax', 'Content');
1448
			$this->getResponse()->addHeader('X-Pjax', 'Content');
1449
1450
			return $this->getResponseNegotiator()->respond($this->getRequest());
1451
		} else {
1452
			return new SS_HTTPResponse("CMSMain::duplicate() Bad ID: '$id'", 400);
1453
		}
1454
	}
1455
1456
	public function duplicatewithchildren($request) {
1457
		// Protect against CSRF on destructive action
1458
		if(!SecurityToken::inst()->checkRequest($request)) return $this->httpError(400);
1459
		increase_time_limit_to();
1460
		if(($id = $this->urlParams['ID']) && is_numeric($id)) {
1461
			$page = SiteTree::get()->byID($id);
1462 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...
Bug introduced by
The method Parent() does not exist on SilverStripe\ORM\DataObject. Did you maybe mean parentClass()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
1463
				return Security::permissionFailure($this);
1464
			}
1465
			if(!$page || !$page->ID) throw new SS_HTTPResponse_Exception("Bad record ID #$id", 404);
1466
1467
			$newPage = $page->duplicateWithChildren();
1468
1469
			$this->getResponse()->addHeader(
1470
				'X-Status',
1471
				rawurlencode(_t(
1472
					'CMSMain.DUPLICATEDWITHCHILDREN',
1473
					"Duplicated '{title}' and children successfully",
1474
					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...
1475
				))
1476
			);
1477
			$url = Controller::join_links(CMSPageEditController::singleton()->Link('show'), $newPage->ID);
1478
			$this->getResponse()->addHeader('X-ControllerURL', $url);
1479
			$this->getRequest()->addHeader('X-Pjax', 'Content');
1480
			$this->getResponse()->addHeader('X-Pjax', 'Content');
1481
1482
			return $this->getResponseNegotiator()->respond($this->getRequest());
1483
		} else {
1484
			return new SS_HTTPResponse("CMSMain::duplicatewithchildren() Bad ID: '$id'", 400);
1485
		}
1486
	}
1487
1488
	public function providePermissions() {
1489
		$title = CMSPagesController::menu_title();
1490
		return array(
1491
			"CMS_ACCESS_CMSMain" => array(
1492
				'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...
1493
				'category' => _t('Permission.CMS_ACCESS_CATEGORY', 'CMS Access'),
1494
				'help' => _t(
1495
					'CMSMain.ACCESS_HELP',
1496
					'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".'
1497
				),
1498
				'sort' => -99 // below "CMS_ACCESS_LeftAndMain", but above everything else
1499
			)
1500
		);
1501
	}
1502
1503
}
1504