Completed
Pull Request — master (#1699)
by Damian
02:27
created

CMSMain::SearchForm()   B

Complexity

Conditions 2
Paths 2

Size

Total Lines 76
Code Lines 46

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 76
rs 8.9667
c 0
b 0
f 0
cc 2
eloc 46
nc 2
nop 0

How to fix   Long Method   

Long Method

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

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

Commonly applied refactorings include:

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

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

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

class ParentClass {
    private $data = array();

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

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

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

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

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

Loading history...
293
		if(!$id) {
294
			$id = $this->currentPageID();
295
		}
296
		return $this->LinkWithSearch(
297
			Controller::join_links(CMSPageEditController::singleton()->Link('show'), $id)
298
		);
299
	}
300
301 View Code Duplication
	public function LinkPageSettings() {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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

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

Loading history...
302
		if($id = $this->currentPageID()) {
303
			return $this->LinkWithSearch(
304
				Controller::join_links(CMSPageSettingsController::singleton()->Link('show'), $id)
305
			);
306
		} else {
307
			return null;
308
		}
309
	}
310
311 View Code Duplication
	public function LinkPageHistory() {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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

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

Loading history...
312
		if($id = $this->currentPageID()) {
313
			return $this->LinkWithSearch(
314
				Controller::join_links(CMSPageHistoryController::singleton()->Link('show'), $id)
315
			);
316
		} else {
317
			return null;
318
		}
319
	}
320
321
	public function LinkWithSearch($link) {
322
		// Whitelist to avoid side effects
323
		$params = array(
324
			'q' => (array)$this->getRequest()->getVar('q'),
325
			'ParentID' => $this->getRequest()->getVar('ParentID')
326
		);
327
		$link = Controller::join_links(
328
			$link,
329
			array_filter(array_values($params)) ? '?' . http_build_query($params) : null
330
		);
331
		$this->extend('updateLinkWithSearch', $link);
332
		return $link;
333
	}
334
335
	public function LinkPageAdd($extra = null, $placeholders = null) {
336
		$link = CMSPageAddController::singleton()->Link();
337
		$this->extend('updateLinkPageAdd', $link);
338
339
		if($extra) {
340
			$link = Controller::join_links ($link, $extra);
341
		}
342
343
		if($placeholders) {
344
			$link .= (strpos($link, '?') === false ? "?$placeholders" : "&$placeholders");
345
		}
346
347
		return $link;
348
	}
349
350
	/**
351
	 * @return string
352
	 */
353
	public function LinkPreview() {
354
		$record = $this->getRecord($this->currentPageID());
355
		$baseLink = Director::absoluteBaseURL();
356
		if ($record && $record instanceof SiteTree) {
357
			// if we are an external redirector don't show a link
358
			if ($record instanceof RedirectorPage && $record->RedirectionType == 'External') {
359
				$baseLink = false;
360
			}
361
			else {
362
				$baseLink = $record->Link('?stage=Stage');
363
			}
364
		}
365
		return $baseLink;
366
	}
367
368
	/**
369
	 * Return the entire site tree as a nested set of ULs
370
	 */
371
	public function SiteTreeAsUL() {
372
		// Pre-cache sitetree version numbers for querying efficiency
373
		Versioned::prepopulate_versionnumber_cache(SiteTree::class, "Stage");
374
		Versioned::prepopulate_versionnumber_cache(SiteTree::class, "Live");
375
		$html = $this->getSiteTreeFor($this->stat('tree_class'));
376
377
		$this->extend('updateSiteTreeAsUL', $html);
378
379
		return $html;
380
	}
381
382
	/**
383
	 * @return boolean
384
	 */
385
	public function TreeIsFiltered() {
386
		$query = $this->getRequest()->getVar('q');
387
388
		if (!$query || (count($query) === 1 && isset($query['FilterClass']) && $query['FilterClass'] === 'SilverStripe\\CMS\\Controllers\\CMSSiteTreeFilter_Search')) {
389
			return false;
390
		}
391
392
		return true;
393
	}
394
395
	public function ExtraTreeTools() {
396
		$html = '';
397
		$this->extend('updateExtraTreeTools', $html);
398
		return $html;
399
	}
400
401
	/**
402
	 * Returns a Form for page searching for use in templates.
403
	 *
404
	 * Can be modified from a decorator by a 'updateSearchForm' method
405
	 *
406
	 * @return Form
407
	 */
408
	public function SearchForm() {
409
		// Create the fields
410
		$content = new TextField('q[Term]', _t('CMSSearch.FILTERLABELTEXT', 'Search'));
411
		$dateFrom = new DateField(
412
			'q[LastEditedFrom]',
413
			_t('CMSSearch.FILTERDATEFROM', 'From')
414
		);
415
		$dateFrom->setConfig('showcalendar', true);
416
		$dateTo = new DateField(
417
			'q[LastEditedTo]',
418
			_t('CMSSearch.FILTERDATETO', 'To')
419
		);
420
		$dateTo->setConfig('showcalendar', true);
421
		$pageFilter = new DropdownField(
422
			'q[FilterClass]',
423
			_t('CMSMain.PAGES', 'Page status'),
424
			CMSSiteTreeFilter::get_all_filters()
425
		);
426
		$pageClasses = new DropdownField(
427
			'q[ClassName]',
428
			_t('CMSMain.PAGETYPEOPT', 'Page type', 'Dropdown for limiting search to a page type'),
429
			$this->getPageTypes()
430
		);
431
		$pageClasses->setEmptyString(_t('CMSMain.PAGETYPEANYOPT','Any'));
432
433
		// Group the Datefields
434
		$dateGroup = new FieldGroup(
435
			$dateFrom,
436
			$dateTo
437
		);
438
		$dateGroup->setTitle(_t('CMSSearch.PAGEFILTERDATEHEADING', 'Last edited'));
439
440
		// view mode
441
		$viewMode = HiddenField::create('view', false, $this->ViewState());
442
443
		// Create the Field list
444
		$fields = new FieldList(
445
			$content,
446
			$pageFilter,
447
			$pageClasses,
448
			$dateGroup,
449
			$viewMode
450
		);
451
452
		// Create the Search and Reset action
453
		$actions = new FieldList(
454
			FormAction::create('doSearch',  _t('CMSMain_left_ss.APPLY_FILTER', 'Search'))
455
				->addExtraClass('btn btn-primary'),
456
			ResetFormAction::create('clear', _t('CMSMain_left_ss.CLEAR_FILTER', 'Clear'))
457
				->addExtraClass('btn btn-secondary')
458
		);
459
460
		// Use <button> to allow full jQuery UI styling on the all of the Actions
461
		/** @var FormAction $action */
462
		foreach($actions->dataFields() as $action) {
463
			/** @var FormAction $action */
464
			$action->setUseButtonTag(true);
465
		}
466
467
		// Create the form
468
		/** @skipUpgrade */
469
		$form = Form::create($this, 'SearchForm', $fields, $actions)
470
			->addExtraClass('cms-search-form')
471
			->setFormMethod('GET')
472
			->setFormAction($this->Link())
473
			->disableSecurityToken()
474
			->unsetValidator();
475
476
		// Load the form with previously sent search data
477
		$form->loadDataFrom($this->getRequest()->getVars());
478
479
		// Allow decorators to modify the form
480
		$this->extend('updateSearchForm', $form);
481
482
		return $form;
483
	}
484
485
	/**
486
	 * Returns a sorted array suitable for a dropdown with pagetypes and their translated name
487
	 *
488
	 * @return array
489
	 */
490
	protected function getPageTypes() {
491
		$pageTypes = array();
492
		foreach(SiteTree::page_type_classes() as $pageTypeClass) {
493
			$pageTypes[$pageTypeClass] = SiteTree::singleton($pageTypeClass)->i18n_singular_name();
494
		}
495
		asort($pageTypes);
496
		return $pageTypes;
497
	}
498
499
	public function doSearch($data, $form) {
0 ignored issues
show
Unused Code introduced by
The parameter $data is not used and could be removed.

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

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

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

Loading history...
500
		return $this->getsubtree($this->getRequest());
501
	}
502
503
	/**
504
	 * @param bool $unlinked
505
	 * @return ArrayList
506
	 */
507
	public function Breadcrumbs($unlinked = false) {
508
		$items = parent::Breadcrumbs($unlinked);
509
510
		if($items->count() > 1) {
511
			// Specific to the SiteTree admin section, we never show the cms section and current
512
			// page in the same breadcrumbs block.
513
			$items->shift();
514
		}
515
516
		return $items;
517
	}
518
519
	/**
520
	 * Create serialized JSON string with site tree hints data to be injected into
521
	 * 'data-hints' attribute of root node of jsTree.
522
	 *
523
	 * @return string Serialized JSON
524
	 */
525
	public function SiteTreeHints() {
526
		$classes = SiteTree::page_type_classes();
527
528
	 	$cacheCanCreate = array();
529
	 	foreach($classes as $class) $cacheCanCreate[$class] = singleton($class)->canCreate();
530
531
	 	// Generate basic cache key. Too complex to encompass all variations
532
	 	$cache = Cache::factory('CMSMain_SiteTreeHints');
533
	 	$cacheKey = md5(implode('_', array(Member::currentUserID(), implode(',', $cacheCanCreate), implode(',', $classes))));
534
	 	if($this->getRequest()->getVar('flush')) {
535
	 		$cache->clean(Zend_Cache::CLEANING_MODE_ALL);
536
		}
537
	 	$json = $cache->load($cacheKey);
538
	 	if(!$json) {
539
			$def['Root'] = array();
0 ignored issues
show
Coding Style Comprehensibility introduced by
$def was never initialized. Although not strictly required by PHP, it is generally a good practice to add $def = array(); before regardless.

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

Let’s take a look at an example:

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

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

    // do something with $myArray
}

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

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

Loading history...
540
			$def['Root']['disallowedChildren'] = array();
541
542
			// Contains all possible classes to support UI controls listing them all,
543
			// such as the "add page here" context menu.
544
			$def['All'] = array();
545
546
			// Identify disallows and set globals
547
			foreach($classes as $class) {
548
				$obj = singleton($class);
549
				if($obj instanceof HiddenClass) continue;
550
551
				// Name item
552
				$def['All'][$class] = array(
553
					'title' => $obj->i18n_singular_name()
554
				);
555
556
				// Check if can be created at the root
557
				$needsPerm = $obj->stat('need_permission');
558
				if(
559
					!$obj->stat('can_be_root')
560
					|| (!array_key_exists($class, $cacheCanCreate) || !$cacheCanCreate[$class])
561
					|| ($needsPerm && !$this->can($needsPerm))
562
				) {
563
					$def['Root']['disallowedChildren'][] = $class;
564
				}
565
566
				// Hint data specific to the class
567
				$def[$class] = array();
568
569
				$defaultChild = $obj->defaultChild();
570
				if($defaultChild !== 'Page' && $defaultChild !== null) {
571
					$def[$class]['defaultChild'] = $defaultChild;
572
				}
573
574
				$defaultParent = $obj->defaultParent();
575
				if ($defaultParent !== 1 && $defaultParent !== null) {
576
					$def[$class]['defaultParent'] = $defaultParent;
577
				}
578
			}
579
580
			$this->extend('updateSiteTreeHints', $def);
581
582
			$json = Convert::raw2json($def);
583
			$cache->save($json, $cacheKey);
584
		}
585
		return $json;
586
	}
587
588
	/**
589
	 * Populates an array of classes in the CMS
590
	 * which allows the user to change the page type.
591
	 *
592
	 * @return SS_List
593
	 */
594
	public function PageTypes() {
595
		$classes = SiteTree::page_type_classes();
596
597
		$result = new ArrayList();
598
599
		foreach($classes as $class) {
600
			$instance = singleton($class);
601
602
			if($instance instanceof HiddenClass) {
603
				continue;
604
			}
605
606
			// skip this type if it is restricted
607
			if($instance->stat('need_permission') && !$this->can(singleton($class)->stat('need_permission'))) {
608
				continue;
609
			}
610
611
			$addAction = $instance->i18n_singular_name();
612
613
			// Get description (convert 'Page' to 'SiteTree' for correct localization lookups)
614
			$i18nClass = ($class == 'Page') ? 'SilverStripe\\CMS\\Model\\SiteTree' : $class;
615
			$description = _t($i18nClass . '.DESCRIPTION');
616
617
			if(!$description) {
618
				$description = $instance->uninherited('description');
619
			}
620
621
			if($class == 'Page' && !$description) {
622
				$description = SiteTree::singleton()->uninherited('description');
623
			}
624
625
			$result->push(new ArrayData(array(
626
				'ClassName' => $class,
627
				'AddAction' => $addAction,
628
				'Description' => $description,
629
				// TODO Sprite support
630
				'IconURL' => $instance->stat('icon'),
631
				'Title' => singleton($class)->i18n_singular_name(),
632
			)));
633
		}
634
635
		$result = $result->sort('AddAction');
636
637
		return $result;
638
	}
639
640
	/**
641
	 * Get a database record to be managed by the CMS.
642
	 *
643
	 * @param int $id Record ID
644
	 * @param int $versionID optional Version id of the given record
645
	 * @return SiteTree
646
	 */
647
 	public function getRecord($id, $versionID = null) {
648
		$treeClass = $this->stat('tree_class');
649
650
		if($id instanceof $treeClass) {
651
			return $id;
652
		}
653
		else if($id && is_numeric($id)) {
654
			$currentStage = Versioned::get_reading_mode();
655
656
			if($this->getRequest()->getVar('Version')) {
657
				$versionID = (int) $this->getRequest()->getVar('Version');
658
			}
659
660
			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...
661
				$record = Versioned::get_version($treeClass, $id, $versionID);
662
			} else {
663
				$record = DataObject::get_by_id($treeClass, $id);
664
			}
665
666
			// Then, try getting a record from the live site
667
			if(!$record) {
668
				// $record = Versioned::get_one_by_stage($treeClass, "Live", "\"$treeClass\".\"ID\" = $id");
669
				Versioned::set_stage(Versioned::LIVE);
670
				singleton($treeClass)->flushCache();
671
672
				$record = DataObject::get_by_id($treeClass, $id);
673
			}
674
675
			// Then, try getting a deleted record
676
			if(!$record) {
677
				$record = Versioned::get_latest_version($treeClass, $id);
678
			}
679
680
			// Don't open a page from a different locale
681
			/** The record's Locale is saved in database in 2.4, and not related with Session,
682
			 *  we should not check their locale matches the Translatable::get_current_locale,
683
			 * 	here as long as we all the HTTPRequest is init with right locale.
684
			 *	This bit breaks the all FileIFrameField functions if the field is used in CMS
685
			 *  and its relevent ajax calles, like loading the tree dropdown for TreeSelectorField.
686
			 */
687
			/* if($record && SiteTree::has_extension('Translatable') && $record->Locale && $record->Locale != Translatable::get_current_locale()) {
688
				$record = null;
689
			}*/
690
691
			// Set the reading mode back to what it was.
692
			Versioned::set_reading_mode($currentStage);
693
694
			return $record;
695
696
		} else if(substr($id,0,3) == 'new') {
697
			return $this->getNewItem($id);
698
		}
699
	}
700
701
	/**
702
	 * @param int $id
703
	 * @param FieldList $fields
704
	 * @return Form
705
	 */
706
	public function getEditForm($id = null, $fields = null) {
707
		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...
708
		$form = parent::getEditForm($id, $fields);
709
710
		// TODO Duplicate record fetching (see parent implementation)
711
		$record = $this->getRecord($id);
712
		if($record && !$record->canView()) return Security::permissionFailure($this);
0 ignored issues
show
Bug Compatibility introduced by
The expression \SilverStripe\Security\S...rmissionFailure($this); of type SilverStripe\Control\HTTPResponse|null adds the type SilverStripe\Control\HTTPResponse to the return on line 712 which is incompatible with the return type documented by SilverStripe\CMS\Controllers\CMSMain::getEditForm of type SilverStripe\Forms\Form.
Loading history...
713
714
		if(!$fields) $fields = $form->Fields();
715
		$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...
716
717
		if($record) {
718
			$deletedFromStage = !$record->isOnDraft();
0 ignored issues
show
Documentation Bug introduced by
The method isOnDraft does not exist on object<SilverStripe\CMS\Model\SiteTree>? Since you implemented __call, maybe consider adding a @method annotation.

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

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

class ParentClass {
    private $data = array();

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

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

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
719
720
			$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...
721
			// Necessary for different subsites
722
			$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...
723
			$fields->push($liveLinkField = new HiddenField("LiveLink"));
724
			$fields->push($stageLinkField = new HiddenField("StageLink"));
725
			$fields->push(new HiddenField("TreeTitle", false, $record->TreeTitle));
0 ignored issues
show
Documentation introduced by
The property TreeTitle does not exist on object<SilverStripe\CMS\Model\SiteTree>. Since you implemented __get, maybe consider adding a @property annotation.

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

<?php

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

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

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

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

}

If the property has read access only, you can use the @property-read annotation instead.

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

See also the PhpDoc documentation for @property.

Loading history...
Documentation introduced by
false is of type boolean, but the function expects a null|string.

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
726
727
			if($record->ID && is_numeric( $record->ID ) ) {
728
				$liveLink = $record->getAbsoluteLiveLink();
729
				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...
730
				if(!$deletedFromStage) {
731
					$stageLink = Controller::join_links($record->AbsoluteLink(), '?stage=Stage');
732
					if($stageLink) $stageLinkField->setValue($stageLink);
733
				}
734
			}
735
736
			// Added in-line to the form, but plucked into different view by LeftAndMain.Preview.js upon load
737
			/** @skipUpgrade */
738
			if($record instanceof CMSPreviewable && !$fields->fieldByName('SilverStripeNavigator')) {
739
				$navField = new LiteralField('SilverStripeNavigator', $this->getSilverStripeNavigator());
740
				$navField->setAllowHTML(true);
741
				$fields->push($navField);
742
			}
743
744
			// getAllCMSActions can be used to completely redefine the action list
745
			if($record->hasMethod('getAllCMSActions')) {
746
				$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...
747
			} else {
748
				$actions = $record->getCMSActions();
749
750
				// Find and remove action menus that have no actions.
751
				if ($actions && $actions->count()) {
752
					/** @var TabSet $tabset */
753
					$tabset = $actions->fieldByName('ActionMenus');
754
					if ($tabset) {
755
						foreach ($tabset->getChildren() as $tab) {
756
							if (!$tab->getChildren()->count()) {
757
								$tabset->removeByName($tab->getName());
758
							}
759
						}
760
					}
761
				}
762
			}
763
764
			// Use <button> to allow full jQuery UI styling
765
			$actionsFlattened = $actions->dataFields();
766
			if($actionsFlattened) {
767
				/** @var FormAction $action */
768
				foreach($actionsFlattened as $action) {
769
					$action->setUseButtonTag(true);
770
				}
771
			}
772
773
			if($record->hasMethod('getCMSValidator')) {
774
				$validator = $record->getCMSValidator();
0 ignored issues
show
Documentation Bug introduced by
The method getCMSValidator does not exist on object<SilverStripe\CMS\Model\SiteTree>? Since you implemented __call, maybe consider adding a @method annotation.

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

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

class ParentClass {
    private $data = array();

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

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

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
Unused Code introduced by
$validator is not used, you could remove the assignment.

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

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

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

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

Loading history...
775
			} else {
776
				$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...
777
			}
778
779
			// TODO Can't merge $FormAttributes in template at the moment
780
			$form->addExtraClass('center ' . $this->BaseCSSClasses());
781
			// Set validation exemptions for specific actions
782
			$form->setValidationExemptActions(array('restore', 'revert', 'deletefromlive', 'delete', 'unpublish', 'rollback', 'doRollback'));
783
784
			// Announce the capability so the frontend can decide whether to allow preview or not.
785
			if ($record instanceof CMSPreviewable) {
786
				$form->addExtraClass('cms-previewable');
787
			}
788
			$form->addExtraClass('fill-height flexbox-area-grow');
789
790
			if(!$record->canEdit() || $deletedFromStage) {
791
				$readonlyFields = $form->Fields()->makeReadonly();
792
				$form->setFields($readonlyFields);
793
			}
794
795
			$form->Fields()->setForm($form);
796
797
			$this->extend('updateEditForm', $form);
798
			return $form;
799
		} else if($id) {
800
			$form = Form::create( $this, "EditForm", new FieldList(
801
				new LabelField('PageDoesntExistLabel',_t('CMSMain.PAGENOTEXISTS',"This page doesn't exist"))), new FieldList()
802
			)->setHTMLID('Form_EditForm');
803
			return $form;
804
		}
805
	}
806
807
	/**
808
	 * @param HTTPRequest $request
809
	 * @return string HTML
810
	 */
811
	public function treeview($request) {
812
		return $this->getResponseNegotiator()->respond($request);
813
	}
814
815
	/**
816
	 * @param HTTPRequest $request
817
	 * @return string HTML
818
	 */
819
	public function listview($request) {
820
		return $this->getResponseNegotiator()->respond($request);
821
	}
822
823
	/**
824
	 * @return string
825
	 */
826
	public function ViewState() {
827
		$mode = $this->getRequest()->requestVar('view')
828
			?: $this->getRequest()->param('Action');
829
		switch($mode) {
830
			case 'listview':
831
			case 'treeview':
832
				return $mode;
833
			default:
834
				return 'treeview';
835
		}
836
	}
837
838
	/**
839
	 * Callback to request the list of page types allowed under a given page instance.
840
	 * Provides a slower but more precise response over SiteTreeHints
841
	 *
842
	 * @param HTTPRequest $request
843
	 * @return HTTPResponse
844
	 */
845
	public function childfilter($request) {
846
		// Check valid parent specified
847
		$parentID = $request->requestVar('ParentID');
848
		$parent = SiteTree::get()->byID($parentID);
849
		if(!$parent || !$parent->exists()) return $this->httpError(404);
850
851
		// Build hints specific to this class
852
		// Identify disallows and set globals
853
		$classes = SiteTree::page_type_classes();
854
		$disallowedChildren = array();
855
		foreach($classes as $class) {
856
			$obj = singleton($class);
857
			if($obj instanceof HiddenClass) continue;
858
859
			if(!$obj->canCreate(null, array('Parent' => $parent))) {
860
				$disallowedChildren[] = $class;
861
			}
862
		}
863
864
		$this->extend('updateChildFilter', $disallowedChildren, $parentID);
865
		return $this
866
			->getResponse()
867
			->addHeader('Content-Type', 'application/json; charset=utf-8')
868
			->setBody(Convert::raw2json($disallowedChildren));
869
	}
870
871
	/**
872
	 * Safely reconstruct a selected filter from a given set of query parameters
873
	 *
874
	 * @param array $params Query parameters to use
875
	 * @return CMSSiteTreeFilter The filter class, or null if none present
876
	 * @throws InvalidArgumentException if invalid filter class is passed.
877
	 */
878
	protected function getQueryFilter($params) {
879
		if(empty($params['FilterClass'])) return null;
880
		$filterClass = $params['FilterClass'];
881
		if(!is_subclass_of($filterClass, 'SilverStripe\\CMS\\Controllers\\CMSSiteTreeFilter')) {
882
			throw new InvalidArgumentException("Invalid filter class passed: {$filterClass}");
883
		}
884
		return $filterClass::create($params);
885
	}
886
887
	/**
888
	 * Returns the pages meet a certain criteria as {@see CMSSiteTreeFilter} or the subpages of a parent page
889
	 * defaulting to no filter and show all pages in first level.
890
	 * Doubles as search results, if any search parameters are set through {@link SearchForm()}.
891
	 *
892
	 * @param array $params Search filter criteria
893
	 * @param int $parentID Optional parent node to filter on (can't be combined with other search criteria)
894
	 * @return SS_List
895
	 * @throws InvalidArgumentException if invalid filter class is passed.
896
	 */
897
	public function getList($params = array(), $parentID = 0) {
898
		if($filter = $this->getQueryFilter($params)) {
899
			return $filter->getFilteredPages();
900
		} else {
901
			$list = DataList::create($this->stat('tree_class'));
902
			$parentID = is_numeric($parentID) ? $parentID : 0;
903
			return $list->filter("ParentID", $parentID);
904
		}
905
	}
906
907
	/**
908
	 * @return Form
909
	 */
910
	public function ListViewForm() {
911
		$params = $this->getRequest()->requestVar('q');
912
		$list = $this->getList($params, $parentID = $this->getRequest()->requestVar('ParentID'));
913
		$gridFieldConfig = GridFieldConfig::create()->addComponents(
914
			new GridFieldSortableHeader(),
915
			new GridFieldDataColumns(),
916
			new GridFieldPaginator(self::config()->page_length)
917
		);
918
		if($parentID){
919
			$linkSpec = $this->Link();
920
			$linkSpec = $linkSpec . (strstr($linkSpec, '?') ? '&' : '?') . 'ParentID=%d&view=listview';
921
			$gridFieldConfig->addComponent(
922
				GridFieldLevelup::create($parentID)
923
					->setLinkSpec($linkSpec)
924
					->setAttributes(array('data-pjax' => 'ListViewForm,Breadcrumbs'))
925
			);
926
		}
927
		$gridField = new GridField('Page','Pages', $list, $gridFieldConfig);
928
		/** @var GridFieldDataColumns $columns */
929
		$columns = $gridField->getConfig()->getComponentByType('SilverStripe\\Forms\\GridField\\GridFieldDataColumns');
930
931
		// Don't allow navigating into children nodes on filtered lists
932
		$fields = array(
933
			'getTreeTitle' => _t('SiteTree.PAGETITLE', 'Page Title'),
934
			'singular_name' => _t('SiteTree.PAGETYPE'),
935
			'LastEdited' => _t('SiteTree.LASTUPDATED', 'Last Updated'),
936
		);
937
		/** @var GridFieldSortableHeader $sortableHeader */
938
		$sortableHeader = $gridField->getConfig()->getComponentByType('SilverStripe\\Forms\\GridField\\GridFieldSortableHeader');
939
		$sortableHeader->setFieldSorting(array('getTreeTitle' => 'Title'));
940
		$gridField->getState()->ParentID = $parentID;
941
942
		if(!$params) {
943
			$fields = array_merge(array('listChildrenLink' => ''), $fields);
944
		}
945
946
		$columns->setDisplayFields($fields);
947
		$columns->setFieldCasting(array(
948
			'Created' => 'DBDatetime->Ago',
949
			'LastEdited' => 'DBDatetime->FormatFromSettings',
950
			'getTreeTitle' => 'HTMLFragment'
951
		));
952
953
		$controller = $this;
954
		$columns->setFieldFormatting(array(
955
			'listChildrenLink' => function($value, &$item) use($controller) {
956
				/** @var SiteTree $item */
957
				$num = $item ? $item->numChildren() : null;
0 ignored issues
show
Documentation Bug introduced by
The method numChildren does not exist on object<SilverStripe\CMS\Model\SiteTree>? Since you implemented __call, maybe consider adding a @method annotation.

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

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

class ParentClass {
    private $data = array();

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

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

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
958
				if($num) {
959
					return sprintf(
960
						'<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>',
961
						Controller::join_links(
962
							$controller->Link(),
963
							sprintf("?ParentID=%d&view=listview", (int)$item->ID)
964
						),
965
						$num
966
					);
967
				}
968
			},
969
			'getTreeTitle' => function($value, &$item) use($controller) {
970
				return sprintf(
971
					'<a class="action-detail" href="%s">%s</a>',
972
					Controller::join_links(
973
						CMSPageEditController::singleton()->Link('show'),
974
						(int)$item->ID
975
					),
976
					$item->TreeTitle // returns HTML, does its own escaping
977
				);
978
			}
979
		));
980
981
		$negotiator = $this->getResponseNegotiator();
982
		$listview = Form::create(
983
			$this,
984
			'ListViewForm',
985
			new FieldList($gridField),
986
			new FieldList()
987
		)->setHTMLID('Form_ListViewForm');
988
		$listview->setAttribute('data-pjax-fragment', 'ListViewForm');
989 View Code Duplication
		$listview->setValidationResponseCallback(function(ValidationResult $errors) use ($negotiator, $listview) {
0 ignored issues
show
Unused Code introduced by
The parameter $errors is not used and could be removed.

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

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

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

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

Loading history...
990
			$request = $this->getRequest();
991
			if($request->isAjax() && $negotiator) {
992
				$result = $listview->forTemplate();
993
				return $negotiator->respond($request, array(
994
					'CurrentForm' => function() use($result) {
995
						return $result;
996
					}
997
				));
998
			}
999
		});
1000
1001
		$this->extend('updateListView', $listview);
1002
1003
		$listview->disableSecurityToken();
1004
		return $listview;
1005
	}
1006
1007
	public function currentPageID() {
1008
		$id = parent::currentPageID();
1009
1010
		$this->extend('updateCurrentPageID', $id);
1011
1012
		return $id;
1013
	}
1014
1015
	//------------------------------------------------------------------------------------------//
1016
	// Data saving handlers
1017
1018
	/**
1019
	 * Save and Publish page handler
1020
	 *
1021
	 * @param array $data
1022
	 * @param Form $form
1023
	 * @return HTTPResponse
1024
	 * @throws HTTPResponse_Exception
1025
	 */
1026
	public function save($data, $form) {
1027
		$className = $this->stat('tree_class');
1028
1029
		// Existing or new record?
1030
		$id = $data['ID'];
1031
		if(substr($id,0,3) != 'new') {
1032
			/** @var SiteTree $record */
1033
			$record = DataObject::get_by_id($className, $id);
1034
			// Check edit permissions
1035
			if($record && !$record->canEdit()) {
1036
				return Security::permissionFailure($this);
1037
			}
1038
			if(!$record || !$record->ID) {
1039
				throw new HTTPResponse_Exception("Bad record ID #$id", 404);
1040
			}
1041
		} else {
1042
			if(!$className::singleton()->canCreate()) {
1043
				return Security::permissionFailure($this);
1044
			}
1045
			$record = $this->getNewItem($id, false);
1046
		}
1047
1048
		// Check publishing permissions
1049
		$doPublish = !empty($data['publish']);
1050
		if($record && $doPublish && !$record->canPublish()) {
1051
			return Security::permissionFailure($this);
1052
		}
1053
1054
		// TODO Coupling to SiteTree
1055
		$record->HasBrokenLink = 0;
1056
		$record->HasBrokenFile = 0;
1057
1058
		if (!$record->ObsoleteClassName) {
1059
			$record->writeWithoutVersion();
1060
		}
1061
1062
		// Update the class instance if necessary
1063
		if(isset($data['ClassName']) && $data['ClassName'] != $record->ClassName) {
1064
			// Replace $record with a new instance of the new class
1065
			$newClassName = $data['ClassName'];
1066
			$record = $record->newClassInstance($newClassName);
1067
		}
1068
1069
		// save form data into record
1070
		$form->saveInto($record);
1071
		$record->write();
1072
1073
		// If the 'Save & Publish' button was clicked, also publish the page
1074
		if ($doPublish) {
1075
			$record->publishRecursive();
1076
			$message = _t(
1077
				'CMSMain.PUBLISHED',
1078
				"Published '{title}' successfully.",
1079
				['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...
1080
			);
1081
		} else {
1082
			$message = _t(
1083
				'CMSMain.SAVED',
1084
				"Saved '{title}' successfully.",
1085
				['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...
1086
			);
1087
		}
1088
1089
		$this->getResponse()->addHeader('X-Status', rawurlencode($message));
1090
		return $this->getResponseNegotiator()->respond($this->getRequest());
1091
	}
1092
1093
	/**
1094
	 * @uses LeftAndMainExtension->augmentNewSiteTreeItem()
1095
	 *
1096
	 * @param int|string $id
1097
	 * @param bool $setID
1098
	 * @return mixed|DataObject
1099
	 * @throws HTTPResponse_Exception
1100
	 */
1101
	public function getNewItem($id, $setID = true) {
1102
		$parentClass = $this->stat('tree_class');
1103
		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...
1104
1105
		if (!is_a($className, $parentClass, true)) {
1106
			$response = Security::permissionFailure($this);
1107
			if (!$response) {
1108
				$response = $this->getResponse();
1109
			}
1110
			throw new HTTPResponse_Exception($response);
1111
		}
1112
1113
		/** @var SiteTree $newItem */
1114
		$newItem = Injector::inst()->create($className);
1115
		if( !$suffix ) {
1116
			$sessionTag = "NewItems." . $parentID . "." . $className;
1117
			if(Session::get($sessionTag)) {
1118
				$suffix = '-' . Session::get($sessionTag);
1119
				Session::set($sessionTag, Session::get($sessionTag) + 1);
1120
			}
1121
			else
1122
				Session::set($sessionTag, 1);
1123
1124
				$id = $id . $suffix;
1125
		}
1126
1127
		$newItem->Title = _t(
1128
			'CMSMain.NEWPAGE',
1129
			"New {pagetype}",'followed by a page type title',
1130
			array('pagetype' => singleton($className)->i18n_singular_name())
1131
		);
1132
		$newItem->ClassName = $className;
1133
		$newItem->ParentID = $parentID;
0 ignored issues
show
Documentation introduced by
The property ParentID does not exist on object<SilverStripe\CMS\Model\SiteTree>. Since you implemented __set, maybe consider adding a @property annotation.

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

<?php

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

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

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

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

}

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

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

See also the PhpDoc documentation for @property.

Loading history...
1134
1135
		// DataObject::fieldExists only checks the current class, not the hierarchy
1136
		// This allows the CMS to set the correct sort value
1137
		if($newItem->castingHelper('Sort')) {
1138
			$newItem->Sort = DB::prepared_query('SELECT MAX("Sort") FROM "SiteTree" WHERE "ParentID" = ?', array($parentID))->value() + 1;
1139
		}
1140
1141
		if($setID) $newItem->ID = $id;
1142
1143
		# Some modules like subsites add extra fields that need to be set when the new item is created
1144
		$this->extend('augmentNewSiteTreeItem', $newItem);
1145
1146
		return $newItem;
1147
	}
1148
1149
	/**
1150
	 * Actually perform the publication step
1151
	 *
1152
	 * @param Versioned|DataObject $record
1153
	 * @return mixed
1154
	 */
1155
	public function performPublish($record) {
1156
		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...
1157
			return Security::permissionFailure($this);
1158
		}
1159
1160
		$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...
1161
	}
1162
1163
	/**
1164
	 * Reverts a page by publishing it to live.
1165
	 * Use {@link restorepage()} if you want to restore a page
1166
	 * which was deleted from draft without publishing.
1167
	 *
1168
	 * @uses SiteTree->doRevertToLive()
1169
	 *
1170
	 * @param array $data
1171
	 * @param Form $form
1172
	 * @return HTTPResponse
1173
	 * @throws HTTPResponse_Exception
1174
	 */
1175
	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...
1176
		if(!isset($data['ID'])) {
1177
			throw new HTTPResponse_Exception("Please pass an ID in the form content", 400);
1178
		}
1179
1180
		$id = (int) $data['ID'];
1181
		$restoredPage = Versioned::get_latest_version(SiteTree::class, $id);
1182
		if(!$restoredPage) {
1183
			throw new HTTPResponse_Exception("SiteTree #$id not found", 400);
1184
		}
1185
1186
		/** @var SiteTree $record */
1187
		$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...
1188
			'"SiteTree_Live"."ID"' => $id
1189
		));
1190
1191
		// a user can restore a page without publication rights, as it just adds a new draft state
1192
		// (this action should just be available when page has been "deleted from draft")
1193
		if($record && !$record->canEdit()) {
1194
			return Security::permissionFailure($this);
1195
		}
1196
		if(!$record || !$record->ID) {
1197
			throw new HTTPResponse_Exception("Bad record ID #$id", 404);
1198
		}
1199
1200
		$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...
1201
1202
		$this->getResponse()->addHeader(
1203
			'X-Status',
1204
			rawurlencode(_t(
1205
				'CMSMain.RESTORED',
1206
				"Restored '{title}' successfully",
1207
				'Param %s is a title',
1208
				array('title' => $record->Title)
1209
			))
1210
		);
1211
1212
		return $this->getResponseNegotiator()->respond($this->getRequest());
1213
	}
1214
1215
	/**
1216
	 * Delete the current page from draft stage.
1217
	 *
1218
	 * @see deletefromlive()
1219
	 *
1220
	 * @param array $data
1221
	 * @param Form $form
1222
	 * @return HTTPResponse
1223
	 * @throws HTTPResponse_Exception
1224
	 */
1225 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...
1226
		$id = $data['ID'];
1227
		$record = SiteTree::get()->byID($id);
1228
		if($record && !$record->canDelete()) {
1229
			return Security::permissionFailure();
1230
		}
1231
		if(!$record || !$record->ID) {
1232
			throw new HTTPResponse_Exception("Bad record ID #$id", 404);
1233
		}
1234
1235
		// Delete record
1236
		$record->delete();
1237
1238
		$this->getResponse()->addHeader(
1239
			'X-Status',
1240
			rawurlencode(sprintf(_t('CMSMain.REMOVEDPAGEFROMDRAFT',"Removed '%s' from the draft site"), $record->Title))
1241
		);
1242
1243
		// Even if the record has been deleted from stage and live, it can be viewed in "archive mode"
1244
		return $this->getResponseNegotiator()->respond($this->getRequest());
1245
	}
1246
1247
	/**
1248
	 * Delete this page from both live and stage
1249
	 *
1250
	 * @param array $data
1251
	 * @param Form $form
1252
	 * @return HTTPResponse
1253
	 * @throws HTTPResponse_Exception
1254
	 */
1255 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...
1256
		$id = $data['ID'];
1257
		/** @var SiteTree $record */
1258
		$record = SiteTree::get()->byID($id);
1259
		if(!$record || !$record->exists()) {
1260
			throw new HTTPResponse_Exception("Bad record ID #$id", 404);
1261
		}
1262
		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...
1263
			return Security::permissionFailure();
1264
		}
1265
1266
		// Archive record
1267
		$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...
1268
1269
		$this->getResponse()->addHeader(
1270
			'X-Status',
1271
			rawurlencode(sprintf(_t('CMSMain.ARCHIVEDPAGE',"Archived page '%s'"), $record->Title))
1272
		);
1273
1274
		// Even if the record has been deleted from stage and live, it can be viewed in "archive mode"
1275
		return $this->getResponseNegotiator()->respond($this->getRequest());
1276
	}
1277
1278
	public function publish($data, $form) {
1279
		$data['publish'] = '1';
1280
1281
		return $this->save($data, $form);
1282
	}
1283
1284
	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...
1285
		$className = $this->stat('tree_class');
1286
		/** @var SiteTree $record */
1287
		$record = DataObject::get_by_id($className, $data['ID']);
1288
1289
		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...
1290
			return Security::permissionFailure($this);
1291
		}
1292
		if(!$record || !$record->ID) {
1293
			throw new HTTPResponse_Exception("Bad record ID #" . (int)$data['ID'], 404);
1294
		}
1295
1296
		$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...
1297
1298
		$this->getResponse()->addHeader(
1299
			'X-Status',
1300
			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...
1301
		);
1302
1303
		return $this->getResponseNegotiator()->respond($this->getRequest());
1304
	}
1305
1306
	/**
1307
	 * @return HTTPResponse
1308
	 */
1309
	public function rollback() {
1310
		return $this->doRollback(array(
1311
			'ID' => $this->currentPageID(),
1312
			'Version' => $this->getRequest()->param('VersionID')
1313
		), null);
0 ignored issues
show
Documentation introduced by
null is of type null, but the function expects a object<SilverStripe\Forms\Form>.

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1314
	}
1315
1316
	/**
1317
	 * Rolls a site back to a given version ID
1318
	 *
1319
	 * @param array $data
1320
	 * @param Form $form
1321
	 * @return HTTPResponse
1322
	 */
1323
	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...
1324
		$this->extend('onBeforeRollback', $data['ID']);
1325
1326
		$id = (isset($data['ID'])) ? (int) $data['ID'] : null;
1327
		$version = (isset($data['Version'])) ? (int) $data['Version'] : null;
1328
1329
		/** @var DataObject|Versioned $record */
1330
		$record = DataObject::get_by_id($this->stat('tree_class'), $id);
1331
		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...
1332
			return Security::permissionFailure($this);
1333
		}
1334
1335
		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...
1336
			$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...
1337
			$message = _t(
1338
				'CMSMain.ROLLEDBACKVERSIONv2',
1339
				"Rolled back to version #%d.",
1340
				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...
1341
			);
1342
		} else {
1343
			$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...
1344
			$message = _t(
1345
				'CMSMain.ROLLEDBACKPUBv2',"Rolled back to published version."
1346
			);
1347
		}
1348
1349
		$this->getResponse()->addHeader('X-Status', rawurlencode($message));
1350
1351
		// Can be used in different contexts: In normal page edit view, in which case the redirect won't have any effect.
1352
		// Or in history view, in which case a revert causes the CMS to re-load the edit view.
1353
		// The X-Pjax header forces a "full" content refresh on redirect.
1354
		$url = Controller::join_links(CMSPageEditController::singleton()->Link('show'), $record->ID);
1355
		$this->getResponse()->addHeader('X-ControllerURL', $url);
1356
		$this->getRequest()->addHeader('X-Pjax', 'Content');
1357
		$this->getResponse()->addHeader('X-Pjax', 'Content');
1358
1359
		return $this->getResponseNegotiator()->respond($this->getRequest());
1360
	}
1361
1362
	/**
1363
	 * Batch Actions Handler
1364
	 */
1365
	public function batchactions() {
1366
		return new CMSBatchActionHandler($this, 'batchactions');
1367
	}
1368
1369
	public function BatchActionParameters() {
1370
		$batchActions = CMSBatchActionHandler::config()->batch_actions;
1371
1372
		$forms = array();
1373
		foreach($batchActions as $urlSegment => $batchAction) {
1374
			$SNG_action = singleton($batchAction);
1375
			if ($SNG_action->canView() && $fieldset = $SNG_action->getParameterFields()) {
1376
				$formHtml = '';
1377
				/** @var FormField $field */
1378
				foreach($fieldset as $field) {
1379
					$formHtml .= $field->Field();
1380
				}
1381
				$forms[$urlSegment] = $formHtml;
1382
			}
1383
		}
1384
		$pageHtml = '';
1385
		foreach($forms as $urlSegment => $html) {
1386
			$pageHtml .= "<div class=\"params\" id=\"BatchActionParameters_$urlSegment\">$html</div>\n\n";
1387
		}
1388
		return new LiteralField("BatchActionParameters", '<div id="BatchActionParameters" style="display:none">'.$pageHtml.'</div>');
1389
	}
1390
	/**
1391
	 * Returns a list of batch actions
1392
	 */
1393
	public function BatchActionList() {
1394
		return $this->batchactions()->batchActionList();
1395
	}
1396
1397
	public function publishall($request) {
1398
		if(!Permission::check('ADMIN')) return Security::permissionFailure($this);
1399
1400
		increase_time_limit_to();
1401
		increase_memory_limit_to();
1402
1403
		$response = "";
1404
1405
		if(isset($this->requestParams['confirm'])) {
1406
			// Protect against CSRF on destructive action
1407
			if(!SecurityToken::inst()->checkRequest($request)) return $this->httpError(400);
1408
1409
			$start = 0;
1410
			$pages = SiteTree::get()->limit("$start,30");
1411
			$count = 0;
1412
			while($pages) {
1413
				/** @var SiteTree $page */
1414
				foreach($pages as $page) {
1415
					if($page && !$page->canPublish()) {
1416
						return Security::permissionFailure($this);
1417
					}
1418
1419
					$page->publishRecursive();
1420
					$page->destroy();
1421
					unset($page);
1422
					$count++;
1423
					$response .= "<li>$count</li>";
1424
				}
1425
				if($pages->count() > 29) {
1426
					$start += 30;
1427
					$pages = SiteTree::get()->limit("$start,30");
1428
				} else {
1429
					break;
1430
				}
1431
			}
1432
			$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...
1433
1434
		} else {
1435
			$token = SecurityToken::inst();
1436
			$fields = new FieldList();
1437
			$token->updateFieldSet($fields);
1438
			$tokenField = $fields->first();
1439
			$tokenHtml = ($tokenField) ? $tokenField->FieldHolder() : '';
1440
			$publishAllDescription = _t(
1441
				'CMSMain.PUBALLFUN2',
1442
				'Pressing this button will do the equivalent of going to every page and pressing "publish".  '
1443
				. 'It\'s intended to be used after there have been massive edits of the content, such as when '
1444
				. 'the site was first built.'
1445
			);
1446
			$response .= '<h1>' . _t('CMSMain.PUBALLFUN','"Publish All" functionality') . '</h1>
1447
				<p>' . $publishAllDescription . '</p>
1448
				<form method="post" action="publishall">
1449
					<input type="submit" name="confirm" value="'
1450
					. _t('CMSMain.PUBALLCONFIRM',"Please publish every page in the site, copying content stage to live",'Confirmation button') .'" />'
1451
					. $tokenHtml .
1452
				'</form>';
1453
		}
1454
1455
		return $response;
1456
	}
1457
1458
	/**
1459
	 * Restore a completely deleted page from the SiteTree_versions table.
1460
	 *
1461
	 * @param array $data
1462
	 * @param Form $form
1463
	 * @return HTTPResponse
1464
	 */
1465
	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...
1466
		if(!isset($data['ID']) || !is_numeric($data['ID'])) {
1467
			return new HTTPResponse("Please pass an ID in the form content", 400);
1468
		}
1469
1470
		$id = (int)$data['ID'];
1471
		/** @var SiteTree $restoredPage */
1472
		$restoredPage = Versioned::get_latest_version(SiteTree::class, $id);
1473
		if(!$restoredPage) {
1474
			return new HTTPResponse("SiteTree #$id not found", 400);
1475
		}
1476
1477
		$restoredPage = $restoredPage->doRestoreToStage();
1478
1479
		$this->getResponse()->addHeader(
1480
			'X-Status',
1481
			rawurlencode(_t(
1482
				'CMSMain.RESTORED',
1483
				"Restored '{title}' successfully",
1484
				array('title' => $restoredPage->Title)
0 ignored issues
show
Documentation introduced by
array('title' => $restoredPage->Title) is of type array<string,string,{"title":"string"}>, but the function expects a string.

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1485
			))
1486
		);
1487
1488
		return $this->getResponseNegotiator()->respond($this->getRequest());
1489
	}
1490
1491
	public function duplicate($request) {
1492
		// Protect against CSRF on destructive action
1493
		if(!SecurityToken::inst()->checkRequest($request)) return $this->httpError(400);
1494
1495
		if(($id = $this->urlParams['ID']) && is_numeric($id)) {
1496
			/** @var SiteTree $page */
1497
			$page = SiteTree::get()->byID($id);
1498 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...
1499
				return Security::permissionFailure($this);
1500
			}
1501
			if(!$page || !$page->ID) throw new HTTPResponse_Exception("Bad record ID #$id", 404);
1502
1503
			$newPage = $page->duplicate();
1504
1505
			// ParentID can be hard-set in the URL.  This is useful for pages with multiple parents
1506
			if(isset($_GET['parentID']) && is_numeric($_GET['parentID'])) {
1507
				$newPage->ParentID = $_GET['parentID'];
1508
				$newPage->write();
1509
			}
1510
1511
			$this->getResponse()->addHeader(
1512
				'X-Status',
1513
				rawurlencode(_t(
1514
					'CMSMain.DUPLICATED',
1515
					"Duplicated '{title}' successfully",
1516
					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...
1517
				))
1518
			);
1519
			$url = Controller::join_links(CMSPageEditController::singleton()->Link('show'), $newPage->ID);
1520
			$this->getResponse()->addHeader('X-ControllerURL', $url);
1521
			$this->getRequest()->addHeader('X-Pjax', 'Content');
1522
			$this->getResponse()->addHeader('X-Pjax', 'Content');
1523
1524
			return $this->getResponseNegotiator()->respond($this->getRequest());
1525
		} else {
1526
			return new HTTPResponse("CMSMain::duplicate() Bad ID: '$id'", 400);
1527
		}
1528
	}
1529
1530
	public function duplicatewithchildren($request) {
1531
		// Protect against CSRF on destructive action
1532
		if(!SecurityToken::inst()->checkRequest($request)) return $this->httpError(400);
1533
		increase_time_limit_to();
1534
		if(($id = $this->urlParams['ID']) && is_numeric($id)) {
1535
			/** @var SiteTree $page */
1536
			$page = SiteTree::get()->byID($id);
1537 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...
1538
				return Security::permissionFailure($this);
1539
			}
1540
			if(!$page || !$page->ID) throw new HTTPResponse_Exception("Bad record ID #$id", 404);
1541
1542
			$newPage = $page->duplicateWithChildren();
1543
1544
			$this->getResponse()->addHeader(
1545
				'X-Status',
1546
				rawurlencode(_t(
1547
					'CMSMain.DUPLICATEDWITHCHILDREN',
1548
					"Duplicated '{title}' and children successfully",
1549
					array('title' => $newPage->Title)
0 ignored issues
show
Documentation introduced by
array('title' => $newPage->Title) is of type array<string,string,{"title":"string"}>, but the function expects a string.

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1550
				))
1551
			);
1552
			$url = Controller::join_links(CMSPageEditController::singleton()->Link('show'), $newPage->ID);
1553
			$this->getResponse()->addHeader('X-ControllerURL', $url);
1554
			$this->getRequest()->addHeader('X-Pjax', 'Content');
1555
			$this->getResponse()->addHeader('X-Pjax', 'Content');
1556
1557
			return $this->getResponseNegotiator()->respond($this->getRequest());
1558
		} else {
1559
			return new HTTPResponse("CMSMain::duplicatewithchildren() Bad ID: '$id'", 400);
1560
		}
1561
	}
1562
1563
	public function providePermissions() {
1564
		$title = CMSPagesController::menu_title();
1565
		return array(
1566
			"CMS_ACCESS_CMSMain" => array(
1567
				'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...
1568
				'category' => _t('Permission.CMS_ACCESS_CATEGORY', 'CMS Access'),
1569
				'help' => _t(
1570
					'CMSMain.ACCESS_HELP',
1571
					'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".'
1572
				),
1573
				'sort' => -99 // below "CMS_ACCESS_LeftAndMain", but above everything else
1574
			)
1575
		);
1576
	}
1577
1578
}
1579