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

CMSMain::duplicatewithchildren()   D

Complexity

Conditions 9
Paths 5

Size

Total Lines 32
Code Lines 22

Duplication

Lines 3
Ratio 9.38 %

Importance

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

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

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

class ParentClass {
    private $data = array();

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

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

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

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

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

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

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

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

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

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

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

Loading history...
312
		if($id = $this->currentPageID()) {
313
			return $this->LinkWithSearch(
314
				Controller::join_links(CMSPageHistoryController::singleton()->Link('show'), $id)
315
			);
316
		} else {
317
			return null;
318
		}
319
	}
320
321
	public function LinkWithSearch($link) {
322
		// Whitelist to avoid side effects
323
		$params = array(
324
			'q' => (array)$this->getRequest()->getVar('q'),
325
			'ParentID' => $this->getRequest()->getVar('ParentID')
326
		);
327
		$link = Controller::join_links(
328
			$link,
329
			array_filter(array_values($params)) ? '?' . http_build_query($params) : null
330
		);
331
		$this->extend('updateLinkWithSearch', $link);
332
		return $link;
333
	}
334
335
	public function LinkPageAdd($extra = null, $placeholders = null) {
336
		$link = CMSPageAddController::singleton()->Link();
337
		$this->extend('updateLinkPageAdd', $link);
338
339
		if($extra) {
340
			$link = Controller::join_links ($link, $extra);
341
		}
342
343
		if($placeholders) {
344
			$link .= (strpos($link, '?') === false ? "?$placeholders" : "&$placeholders");
345
		}
346
347
		return $link;
348
	}
349
350
	/**
351
	 * @return string
352
	 */
353
	public function LinkPreview() {
354
		$record = $this->getRecord($this->currentPageID());
355
		$baseLink = Director::absoluteBaseURL();
356
		if ($record && $record instanceof SiteTree) {
357
			// if we are an external redirector don't show a link
358
			if ($record instanceof RedirectorPage && $record->RedirectionType == 'External') {
359
				$baseLink = false;
360
			}
361
			else {
362
				$baseLink = $record->Link('?stage=Stage');
363
			}
364
		}
365
		return $baseLink;
366
	}
367
368
	/**
369
	 * Return the entire site tree as a nested set of ULs
370
	 */
371
	public function SiteTreeAsUL() {
372
		// Pre-cache sitetree version numbers for querying efficiency
373
		Versioned::prepopulate_versionnumber_cache(SiteTree::class, "Stage");
374
		Versioned::prepopulate_versionnumber_cache(SiteTree::class, "Live");
375
		$html = $this->getSiteTreeFor($this->stat('tree_class'));
376
377
		$this->extend('updateSiteTreeAsUL', $html);
378
379
		return $html;
380
	}
381
382
	/**
383
	 * @return boolean
384
	 */
385
	public function TreeIsFiltered() {
386
		$query = $this->getRequest()->getVar('q');
387
388
		if (!$query || (count($query) === 1 && isset($query['FilterClass']) && $query['FilterClass'] === 'SilverStripe\\CMS\\Controllers\\CMSSiteTreeFilter_Search')) {
389
			return false;
390
		}
391
392
		return true;
393
	}
394
395
	public function ExtraTreeTools() {
396
		$html = '';
397
		$this->extend('updateExtraTreeTools', $html);
398
		return $html;
399
	}
400
401
	/**
402
	 * Returns a Form for page searching for use in templates.
403
	 *
404
	 * Can be modified from a decorator by a 'updateSearchForm' method
405
	 *
406
	 * @return Form
407
	 */
408
	public function SearchForm() {
409
		// Create the fields
410
		$content = new TextField('q[Term]', _t('CMSSearch.FILTERLABELTEXT', 'Search'));
411
		$dateFrom = new DateField(
412
			'q[LastEditedFrom]',
413
			_t('CMSSearch.FILTERDATEFROM', 'From')
414
		);
415
		$dateFrom->setConfig('showcalendar', true);
416
		$dateTo = new DateField(
417
			'q[LastEditedTo]',
418
			_t('CMSSearch.FILTERDATETO', 'To')
419
		);
420
		$dateTo->setConfig('showcalendar', true);
421
		$pageFilter = new DropdownField(
422
			'q[FilterClass]',
423
			_t('CMSMain.PAGES', 'Page status'),
424
			CMSSiteTreeFilter::get_all_filters()
425
		);
426
		$pageClasses = new DropdownField(
427
			'q[ClassName]',
428
			_t('CMSMain.PAGETYPEOPT', 'Page type'),
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
            /** @var SiteTree $instance */
601
            $instance = singleton($class);
602
603
			if($instance instanceof HiddenClass) {
604
				continue;
605
			}
606
607
			// skip this type if it is restricted
608
			if($instance->stat('need_permission') && !$this->can(singleton($class)->stat('need_permission'))) {
609
				continue;
610
			}
611
612
			$singularName = $instance->i18n_singular_name();
613
			$description = $instance->i18n_description();
614
615
			$result->push(new ArrayData(array(
616
				'ClassName' => $class,
617
				'AddAction' => $singularName,
618
				'Description' => $description,
619
				// TODO Sprite support
620
				'IconURL' => $instance->stat('icon'),
621
				'Title' => $singularName,
622
			)));
623
		}
624
625
		$result = $result->sort('AddAction');
626
627
		return $result;
628
	}
629
630
	/**
631
	 * Get a database record to be managed by the CMS.
632
	 *
633
	 * @param int $id Record ID
634
	 * @param int $versionID optional Version id of the given record
635
	 * @return SiteTree
636
	 */
637
 	public function getRecord($id, $versionID = null) {
638
		$treeClass = $this->stat('tree_class');
639
640
		if($id instanceof $treeClass) {
641
			return $id;
642
		}
643
		else if($id && is_numeric($id)) {
644
			$currentStage = Versioned::get_reading_mode();
645
646
			if($this->getRequest()->getVar('Version')) {
647
				$versionID = (int) $this->getRequest()->getVar('Version');
648
			}
649
650
			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...
651
				$record = Versioned::get_version($treeClass, $id, $versionID);
652
			} else {
653
				$record = DataObject::get_by_id($treeClass, $id);
654
			}
655
656
			// Then, try getting a record from the live site
657
			if(!$record) {
658
				// $record = Versioned::get_one_by_stage($treeClass, "Live", "\"$treeClass\".\"ID\" = $id");
659
				Versioned::set_stage(Versioned::LIVE);
660
				singleton($treeClass)->flushCache();
661
662
				$record = DataObject::get_by_id($treeClass, $id);
663
			}
664
665
			// Then, try getting a deleted record
666
			if(!$record) {
667
				$record = Versioned::get_latest_version($treeClass, $id);
668
			}
669
670
			// Don't open a page from a different locale
671
			/** The record's Locale is saved in database in 2.4, and not related with Session,
672
			 *  we should not check their locale matches the Translatable::get_current_locale,
673
			 * 	here as long as we all the HTTPRequest is init with right locale.
674
			 *	This bit breaks the all FileIFrameField functions if the field is used in CMS
675
			 *  and its relevent ajax calles, like loading the tree dropdown for TreeSelectorField.
676
			 */
677
			/* if($record && SiteTree::has_extension('Translatable') && $record->Locale && $record->Locale != Translatable::get_current_locale()) {
678
				$record = null;
679
			}*/
680
681
			// Set the reading mode back to what it was.
682
			Versioned::set_reading_mode($currentStage);
683
684
			return $record;
685
686
		} else if(substr($id,0,3) == 'new') {
687
			return $this->getNewItem($id);
688
		}
689
	}
690
691
	/**
692
	 * @param int $id
693
	 * @param FieldList $fields
694
	 * @return Form
695
	 */
696
	public function getEditForm($id = null, $fields = null) {
697
		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...
698
		$form = parent::getEditForm($id, $fields);
699
700
		// TODO Duplicate record fetching (see parent implementation)
701
		$record = $this->getRecord($id);
702
		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 702 which is incompatible with the return type documented by SilverStripe\CMS\Controllers\CMSMain::getEditForm of type SilverStripe\Forms\Form.
Loading history...
703
704
		if(!$fields) $fields = $form->Fields();
705
		$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...
706
707
		if($record) {
708
			$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...
709
710
			$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...
711
			// Necessary for different subsites
712
			$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...
713
			$fields->push($liveLinkField = new HiddenField("LiveLink"));
714
			$fields->push($stageLinkField = new HiddenField("StageLink"));
715
			$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...
716
717
			if($record->ID && is_numeric( $record->ID ) ) {
718
				$liveLink = $record->getAbsoluteLiveLink();
719
				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...
720
				if(!$deletedFromStage) {
721
					$stageLink = Controller::join_links($record->AbsoluteLink(), '?stage=Stage');
722
					if($stageLink) $stageLinkField->setValue($stageLink);
723
				}
724
			}
725
726
			// Added in-line to the form, but plucked into different view by LeftAndMain.Preview.js upon load
727
			/** @skipUpgrade */
728
			if($record instanceof CMSPreviewable && !$fields->fieldByName('SilverStripeNavigator')) {
729
				$navField = new LiteralField('SilverStripeNavigator', $this->getSilverStripeNavigator());
730
				$navField->setAllowHTML(true);
731
				$fields->push($navField);
732
			}
733
734
			// getAllCMSActions can be used to completely redefine the action list
735
			if($record->hasMethod('getAllCMSActions')) {
736
				$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...
737
			} else {
738
				$actions = $record->getCMSActions();
739
740
				// Find and remove action menus that have no actions.
741
				if ($actions && $actions->count()) {
742
					/** @var TabSet $tabset */
743
					$tabset = $actions->fieldByName('ActionMenus');
744
					if ($tabset) {
745
						foreach ($tabset->getChildren() as $tab) {
746
							if (!$tab->getChildren()->count()) {
747
								$tabset->removeByName($tab->getName());
748
							}
749
						}
750
					}
751
				}
752
			}
753
754
			// Use <button> to allow full jQuery UI styling
755
			$actionsFlattened = $actions->dataFields();
756
			if($actionsFlattened) {
757
				/** @var FormAction $action */
758
				foreach($actionsFlattened as $action) {
759
					$action->setUseButtonTag(true);
760
				}
761
			}
762
763
			if($record->hasMethod('getCMSValidator')) {
764
				$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...
765
			} else {
766
				$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...
767
			}
768
769
			// TODO Can't merge $FormAttributes in template at the moment
770
			$form->addExtraClass('center ' . $this->BaseCSSClasses());
771
			// Set validation exemptions for specific actions
772
			$form->setValidationExemptActions(array('restore', 'revert', 'deletefromlive', 'delete', 'unpublish', 'rollback', 'doRollback'));
773
774
			// Announce the capability so the frontend can decide whether to allow preview or not.
775
			if ($record instanceof CMSPreviewable) {
776
				$form->addExtraClass('cms-previewable');
777
			}
778
			$form->addExtraClass('fill-height flexbox-area-grow');
779
780
			if(!$record->canEdit() || $deletedFromStage) {
781
				$readonlyFields = $form->Fields()->makeReadonly();
782
				$form->setFields($readonlyFields);
783
			}
784
785
			$form->Fields()->setForm($form);
786
787
			$this->extend('updateEditForm', $form);
788
			return $form;
789
		} else if($id) {
790
			$form = Form::create( $this, "EditForm", new FieldList(
791
				new LabelField('PageDoesntExistLabel',_t('CMSMain.PAGENOTEXISTS',"This page doesn't exist"))), new FieldList()
792
			)->setHTMLID('Form_EditForm');
793
			return $form;
794
		}
795
	}
796
797
	/**
798
	 * @param HTTPRequest $request
799
	 * @return string HTML
800
	 */
801
	public function treeview($request) {
802
		return $this->getResponseNegotiator()->respond($request);
803
	}
804
805
	/**
806
	 * @param HTTPRequest $request
807
	 * @return string HTML
808
	 */
809
	public function listview($request) {
810
		return $this->getResponseNegotiator()->respond($request);
811
	}
812
813
	/**
814
	 * @return string
815
	 */
816
	public function ViewState() {
817
		$mode = $this->getRequest()->requestVar('view')
818
			?: $this->getRequest()->param('Action');
819
		switch($mode) {
820
			case 'listview':
821
			case 'treeview':
822
				return $mode;
823
			default:
824
				return 'treeview';
825
		}
826
	}
827
828
	/**
829
	 * Callback to request the list of page types allowed under a given page instance.
830
	 * Provides a slower but more precise response over SiteTreeHints
831
	 *
832
	 * @param HTTPRequest $request
833
	 * @return HTTPResponse
834
	 */
835
	public function childfilter($request) {
836
		// Check valid parent specified
837
		$parentID = $request->requestVar('ParentID');
838
		$parent = SiteTree::get()->byID($parentID);
839
		if(!$parent || !$parent->exists()) return $this->httpError(404);
840
841
		// Build hints specific to this class
842
		// Identify disallows and set globals
843
		$classes = SiteTree::page_type_classes();
844
		$disallowedChildren = array();
845
		foreach($classes as $class) {
846
			$obj = singleton($class);
847
			if($obj instanceof HiddenClass) continue;
848
849
			if(!$obj->canCreate(null, array('Parent' => $parent))) {
850
				$disallowedChildren[] = $class;
851
			}
852
		}
853
854
		$this->extend('updateChildFilter', $disallowedChildren, $parentID);
855
		return $this
856
			->getResponse()
857
			->addHeader('Content-Type', 'application/json; charset=utf-8')
858
			->setBody(Convert::raw2json($disallowedChildren));
859
	}
860
861
	/**
862
	 * Safely reconstruct a selected filter from a given set of query parameters
863
	 *
864
	 * @param array $params Query parameters to use
865
	 * @return CMSSiteTreeFilter The filter class, or null if none present
866
	 * @throws InvalidArgumentException if invalid filter class is passed.
867
	 */
868
	protected function getQueryFilter($params) {
869
		if(empty($params['FilterClass'])) return null;
870
		$filterClass = $params['FilterClass'];
871
		if(!is_subclass_of($filterClass, 'SilverStripe\\CMS\\Controllers\\CMSSiteTreeFilter')) {
872
			throw new InvalidArgumentException("Invalid filter class passed: {$filterClass}");
873
		}
874
		return $filterClass::create($params);
875
	}
876
877
	/**
878
	 * Returns the pages meet a certain criteria as {@see CMSSiteTreeFilter} or the subpages of a parent page
879
	 * defaulting to no filter and show all pages in first level.
880
	 * Doubles as search results, if any search parameters are set through {@link SearchForm()}.
881
	 *
882
	 * @param array $params Search filter criteria
883
	 * @param int $parentID Optional parent node to filter on (can't be combined with other search criteria)
884
	 * @return SS_List
885
	 * @throws InvalidArgumentException if invalid filter class is passed.
886
	 */
887
	public function getList($params = array(), $parentID = 0) {
888
		if($filter = $this->getQueryFilter($params)) {
889
			return $filter->getFilteredPages();
890
		} else {
891
			$list = DataList::create($this->stat('tree_class'));
892
			$parentID = is_numeric($parentID) ? $parentID : 0;
893
			return $list->filter("ParentID", $parentID);
894
		}
895
	}
896
897
	/**
898
	 * @return Form
899
	 */
900
	public function ListViewForm() {
901
		$params = $this->getRequest()->requestVar('q');
902
		$list = $this->getList($params, $parentID = $this->getRequest()->requestVar('ParentID'));
903
		$gridFieldConfig = GridFieldConfig::create()->addComponents(
904
			new GridFieldSortableHeader(),
905
			new GridFieldDataColumns(),
906
			new GridFieldPaginator(self::config()->page_length)
907
		);
908
		if($parentID){
909
			$linkSpec = $this->Link();
910
			$linkSpec = $linkSpec . (strstr($linkSpec, '?') ? '&' : '?') . 'ParentID=%d&view=listview';
911
			$gridFieldConfig->addComponent(
912
				GridFieldLevelup::create($parentID)
913
					->setLinkSpec($linkSpec)
914
					->setAttributes(array('data-pjax' => 'ListViewForm,Breadcrumbs'))
915
			);
916
		}
917
		$gridField = new GridField('Page','Pages', $list, $gridFieldConfig);
918
		/** @var GridFieldDataColumns $columns */
919
		$columns = $gridField->getConfig()->getComponentByType('SilverStripe\\Forms\\GridField\\GridFieldDataColumns');
920
921
		// Don't allow navigating into children nodes on filtered lists
922
		$fields = array(
923
			'getTreeTitle' => _t('SiteTree.PAGETITLE', 'Page Title'),
924
			'singular_name' => _t('SiteTree.PAGETYPE', 'Page Type'),
925
			'LastEdited' => _t('SiteTree.LASTUPDATED', 'Last Updated'),
926
		);
927
		/** @var GridFieldSortableHeader $sortableHeader */
928
		$sortableHeader = $gridField->getConfig()->getComponentByType('SilverStripe\\Forms\\GridField\\GridFieldSortableHeader');
929
		$sortableHeader->setFieldSorting(array('getTreeTitle' => 'Title'));
930
		$gridField->getState()->ParentID = $parentID;
931
932
		if(!$params) {
933
			$fields = array_merge(array('listChildrenLink' => ''), $fields);
934
		}
935
936
		$columns->setDisplayFields($fields);
937
		$columns->setFieldCasting(array(
938
			'Created' => 'DBDatetime->Ago',
939
			'LastEdited' => 'DBDatetime->FormatFromSettings',
940
			'getTreeTitle' => 'HTMLFragment'
941
		));
942
943
		$controller = $this;
944
		$columns->setFieldFormatting(array(
945
			'listChildrenLink' => function($value, &$item) use($controller) {
946
				/** @var SiteTree $item */
947
				$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...
948
				if($num) {
949
					return sprintf(
950
						'<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>',
951
						Controller::join_links(
952
							$controller->Link(),
953
							sprintf("?ParentID=%d&view=listview", (int)$item->ID)
954
						),
955
						$num
956
					);
957
				}
958
			},
959
			'getTreeTitle' => function($value, &$item) use($controller) {
960
				return sprintf(
961
					'<a class="action-detail" href="%s">%s</a>',
962
					Controller::join_links(
963
						CMSPageEditController::singleton()->Link('show'),
964
						(int)$item->ID
965
					),
966
					$item->TreeTitle // returns HTML, does its own escaping
967
				);
968
			}
969
		));
970
971
		$negotiator = $this->getResponseNegotiator();
972
		$listview = Form::create(
973
			$this,
974
			'ListViewForm',
975
			new FieldList($gridField),
976
			new FieldList()
977
		)->setHTMLID('Form_ListViewForm');
978
		$listview->setAttribute('data-pjax-fragment', 'ListViewForm');
979 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...
980
			$request = $this->getRequest();
981
			if($request->isAjax() && $negotiator) {
982
				$result = $listview->forTemplate();
983
				return $negotiator->respond($request, array(
984
					'CurrentForm' => function() use($result) {
985
						return $result;
986
					}
987
				));
988
			}
989
		});
990
991
		$this->extend('updateListView', $listview);
992
993
		$listview->disableSecurityToken();
994
		return $listview;
995
	}
996
997
	public function currentPageID() {
998
		$id = parent::currentPageID();
999
1000
		$this->extend('updateCurrentPageID', $id);
1001
1002
		return $id;
1003
	}
1004
1005
	//------------------------------------------------------------------------------------------//
1006
	// Data saving handlers
1007
1008
	/**
1009
	 * Save and Publish page handler
1010
	 *
1011
	 * @param array $data
1012
	 * @param Form $form
1013
	 * @return HTTPResponse
1014
	 * @throws HTTPResponse_Exception
1015
	 */
1016
	public function save($data, $form) {
1017
		$className = $this->stat('tree_class');
1018
1019
		// Existing or new record?
1020
		$id = $data['ID'];
1021
		if(substr($id,0,3) != 'new') {
1022
			/** @var SiteTree $record */
1023
			$record = DataObject::get_by_id($className, $id);
1024
			// Check edit permissions
1025
			if($record && !$record->canEdit()) {
1026
				return Security::permissionFailure($this);
1027
			}
1028
			if(!$record || !$record->ID) {
1029
				throw new HTTPResponse_Exception("Bad record ID #$id", 404);
1030
			}
1031
		} else {
1032
			if(!$className::singleton()->canCreate()) {
1033
				return Security::permissionFailure($this);
1034
			}
1035
			$record = $this->getNewItem($id, false);
1036
		}
1037
1038
		// Check publishing permissions
1039
		$doPublish = !empty($data['publish']);
1040
		if($record && $doPublish && !$record->canPublish()) {
1041
			return Security::permissionFailure($this);
1042
		}
1043
1044
		// TODO Coupling to SiteTree
1045
		$record->HasBrokenLink = 0;
1046
		$record->HasBrokenFile = 0;
1047
1048
		if (!$record->ObsoleteClassName) {
1049
			$record->writeWithoutVersion();
1050
		}
1051
1052
		// Update the class instance if necessary
1053
		if(isset($data['ClassName']) && $data['ClassName'] != $record->ClassName) {
1054
			// Replace $record with a new instance of the new class
1055
			$newClassName = $data['ClassName'];
1056
			$record = $record->newClassInstance($newClassName);
1057
		}
1058
1059
		// save form data into record
1060
		$form->saveInto($record);
1061
		$record->write();
1062
1063
		// If the 'Save & Publish' button was clicked, also publish the page
1064
		if ($doPublish) {
1065
			$record->publishRecursive();
1066
			$message = _t(
1067
				'CMSMain.PUBLISHED',
1068
				"Published '{title}' successfully.",
1069
				['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...
1070
			);
1071
		} else {
1072
			$message = _t(
1073
				'CMSMain.SAVED',
1074
				"Saved '{title}' successfully.",
1075
				['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...
1076
			);
1077
		}
1078
1079
		$this->getResponse()->addHeader('X-Status', rawurlencode($message));
1080
		return $this->getResponseNegotiator()->respond($this->getRequest());
1081
	}
1082
1083
	/**
1084
	 * @uses LeftAndMainExtension->augmentNewSiteTreeItem()
1085
	 *
1086
	 * @param int|string $id
1087
	 * @param bool $setID
1088
	 * @return mixed|DataObject
1089
	 * @throws HTTPResponse_Exception
1090
	 */
1091
	public function getNewItem($id, $setID = true) {
1092
		$parentClass = $this->stat('tree_class');
1093
		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...
1094
1095
		if (!is_a($className, $parentClass, true)) {
1096
			$response = Security::permissionFailure($this);
1097
			if (!$response) {
1098
				$response = $this->getResponse();
1099
			}
1100
			throw new HTTPResponse_Exception($response);
1101
		}
1102
1103
		/** @var SiteTree $newItem */
1104
		$newItem = Injector::inst()->create($className);
1105
		if( !$suffix ) {
1106
			$sessionTag = "NewItems." . $parentID . "." . $className;
1107
			if(Session::get($sessionTag)) {
1108
				$suffix = '-' . Session::get($sessionTag);
1109
				Session::set($sessionTag, Session::get($sessionTag) + 1);
1110
			}
1111
			else
1112
				Session::set($sessionTag, 1);
1113
1114
				$id = $id . $suffix;
1115
		}
1116
1117
		$newItem->Title = _t(
1118
			'CMSMain.NEWPAGE',
1119
			"New {pagetype}",'followed by a page type title',
1120
			array('pagetype' => singleton($className)->i18n_singular_name())
1121
		);
1122
		$newItem->ClassName = $className;
1123
		$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...
1124
1125
		// DataObject::fieldExists only checks the current class, not the hierarchy
1126
		// This allows the CMS to set the correct sort value
1127
		if($newItem->castingHelper('Sort')) {
1128
			$newItem->Sort = DB::prepared_query('SELECT MAX("Sort") FROM "SiteTree" WHERE "ParentID" = ?', array($parentID))->value() + 1;
1129
		}
1130
1131
		if($setID) $newItem->ID = $id;
1132
1133
		# Some modules like subsites add extra fields that need to be set when the new item is created
1134
		$this->extend('augmentNewSiteTreeItem', $newItem);
1135
1136
		return $newItem;
1137
	}
1138
1139
	/**
1140
	 * Actually perform the publication step
1141
	 *
1142
	 * @param Versioned|DataObject $record
1143
	 * @return mixed
1144
	 */
1145
	public function performPublish($record) {
1146
		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...
1147
			return Security::permissionFailure($this);
1148
		}
1149
1150
		$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...
1151
	}
1152
1153
	/**
1154
	 * Reverts a page by publishing it to live.
1155
	 * Use {@link restorepage()} if you want to restore a page
1156
	 * which was deleted from draft without publishing.
1157
	 *
1158
	 * @uses SiteTree->doRevertToLive()
1159
	 *
1160
	 * @param array $data
1161
	 * @param Form $form
1162
	 * @return HTTPResponse
1163
	 * @throws HTTPResponse_Exception
1164
	 */
1165
	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...
1166
		if(!isset($data['ID'])) {
1167
			throw new HTTPResponse_Exception("Please pass an ID in the form content", 400);
1168
		}
1169
1170
		$id = (int) $data['ID'];
1171
		$restoredPage = Versioned::get_latest_version(SiteTree::class, $id);
1172
		if(!$restoredPage) {
1173
			throw new HTTPResponse_Exception("SiteTree #$id not found", 400);
1174
		}
1175
1176
		/** @var SiteTree $record */
1177
		$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...
1178
			'"SiteTree_Live"."ID"' => $id
1179
		));
1180
1181
		// a user can restore a page without publication rights, as it just adds a new draft state
1182
		// (this action should just be available when page has been "deleted from draft")
1183
		if($record && !$record->canEdit()) {
1184
			return Security::permissionFailure($this);
1185
		}
1186
		if(!$record || !$record->ID) {
1187
			throw new HTTPResponse_Exception("Bad record ID #$id", 404);
1188
		}
1189
1190
		$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...
1191
1192
		$this->getResponse()->addHeader(
1193
			'X-Status',
1194
			rawurlencode(_t(
1195
				'CMSMain.RESTORED',
1196
				"Restored '{title}' successfully",
1197
				'Param %s is a title',
1198
				array('title' => $record->Title)
1199
			))
1200
		);
1201
1202
		return $this->getResponseNegotiator()->respond($this->getRequest());
1203
	}
1204
1205
	/**
1206
	 * Delete the current page from draft stage.
1207
	 *
1208
	 * @see deletefromlive()
1209
	 *
1210
	 * @param array $data
1211
	 * @param Form $form
1212
	 * @return HTTPResponse
1213
	 * @throws HTTPResponse_Exception
1214
	 */
1215 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...
1216
		$id = $data['ID'];
1217
		$record = SiteTree::get()->byID($id);
1218
		if($record && !$record->canDelete()) {
1219
			return Security::permissionFailure();
1220
		}
1221
		if(!$record || !$record->ID) {
1222
			throw new HTTPResponse_Exception("Bad record ID #$id", 404);
1223
		}
1224
1225
		// Delete record
1226
		$record->delete();
1227
1228
		$this->getResponse()->addHeader(
1229
			'X-Status',
1230
			rawurlencode(sprintf(_t('CMSMain.REMOVEDPAGEFROMDRAFT',"Removed '%s' from the draft site"), $record->Title))
1231
		);
1232
1233
		// Even if the record has been deleted from stage and live, it can be viewed in "archive mode"
1234
		return $this->getResponseNegotiator()->respond($this->getRequest());
1235
	}
1236
1237
	/**
1238
	 * Delete this page from both live and stage
1239
	 *
1240
	 * @param array $data
1241
	 * @param Form $form
1242
	 * @return HTTPResponse
1243
	 * @throws HTTPResponse_Exception
1244
	 */
1245 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...
1246
		$id = $data['ID'];
1247
		/** @var SiteTree $record */
1248
		$record = SiteTree::get()->byID($id);
1249
		if(!$record || !$record->exists()) {
1250
			throw new HTTPResponse_Exception("Bad record ID #$id", 404);
1251
		}
1252
		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...
1253
			return Security::permissionFailure();
1254
		}
1255
1256
		// Archive record
1257
		$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...
1258
1259
		$this->getResponse()->addHeader(
1260
			'X-Status',
1261
			rawurlencode(sprintf(_t('CMSMain.ARCHIVEDPAGE',"Archived page '%s'"), $record->Title))
1262
		);
1263
1264
		// Even if the record has been deleted from stage and live, it can be viewed in "archive mode"
1265
		return $this->getResponseNegotiator()->respond($this->getRequest());
1266
	}
1267
1268
	public function publish($data, $form) {
1269
		$data['publish'] = '1';
1270
1271
		return $this->save($data, $form);
1272
	}
1273
1274
	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...
1275
		$className = $this->stat('tree_class');
1276
		/** @var SiteTree $record */
1277
		$record = DataObject::get_by_id($className, $data['ID']);
1278
1279
		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...
1280
			return Security::permissionFailure($this);
1281
		}
1282
		if(!$record || !$record->ID) {
1283
			throw new HTTPResponse_Exception("Bad record ID #" . (int)$data['ID'], 404);
1284
		}
1285
1286
		$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...
1287
1288
		$this->getResponse()->addHeader(
1289
			'X-Status',
1290
			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...
1291
		);
1292
1293
		return $this->getResponseNegotiator()->respond($this->getRequest());
1294
	}
1295
1296
	/**
1297
	 * @return HTTPResponse
1298
	 */
1299
	public function rollback() {
1300
		return $this->doRollback(array(
1301
			'ID' => $this->currentPageID(),
1302
			'Version' => $this->getRequest()->param('VersionID')
1303
		), 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...
1304
	}
1305
1306
	/**
1307
	 * Rolls a site back to a given version ID
1308
	 *
1309
	 * @param array $data
1310
	 * @param Form $form
1311
	 * @return HTTPResponse
1312
	 */
1313
	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...
1314
		$this->extend('onBeforeRollback', $data['ID']);
1315
1316
		$id = (isset($data['ID'])) ? (int) $data['ID'] : null;
1317
		$version = (isset($data['Version'])) ? (int) $data['Version'] : null;
1318
1319
		/** @var DataObject|Versioned $record */
1320
		$record = DataObject::get_by_id($this->stat('tree_class'), $id);
1321
		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...
1322
			return Security::permissionFailure($this);
1323
		}
1324
1325
		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...
1326
			$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...
1327
			$message = _t(
1328
				'CMSMain.ROLLEDBACKVERSIONv2',
1329
				"Rolled back to version #%d.",
1330
				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...
1331
			);
1332
		} else {
1333
			$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...
1334
			$message = _t(
1335
				'CMSMain.ROLLEDBACKPUBv2',"Rolled back to published version."
1336
			);
1337
		}
1338
1339
		$this->getResponse()->addHeader('X-Status', rawurlencode($message));
1340
1341
		// Can be used in different contexts: In normal page edit view, in which case the redirect won't have any effect.
1342
		// Or in history view, in which case a revert causes the CMS to re-load the edit view.
1343
		// The X-Pjax header forces a "full" content refresh on redirect.
1344
		$url = Controller::join_links(CMSPageEditController::singleton()->Link('show'), $record->ID);
1345
		$this->getResponse()->addHeader('X-ControllerURL', $url);
1346
		$this->getRequest()->addHeader('X-Pjax', 'Content');
1347
		$this->getResponse()->addHeader('X-Pjax', 'Content');
1348
1349
		return $this->getResponseNegotiator()->respond($this->getRequest());
1350
	}
1351
1352
	/**
1353
	 * Batch Actions Handler
1354
	 */
1355
	public function batchactions() {
1356
		return new CMSBatchActionHandler($this, 'batchactions');
1357
	}
1358
1359
	public function BatchActionParameters() {
1360
		$batchActions = CMSBatchActionHandler::config()->batch_actions;
1361
1362
		$forms = array();
1363
		foreach($batchActions as $urlSegment => $batchAction) {
1364
			$SNG_action = singleton($batchAction);
1365
			if ($SNG_action->canView() && $fieldset = $SNG_action->getParameterFields()) {
1366
				$formHtml = '';
1367
				/** @var FormField $field */
1368
				foreach($fieldset as $field) {
1369
					$formHtml .= $field->Field();
1370
				}
1371
				$forms[$urlSegment] = $formHtml;
1372
			}
1373
		}
1374
		$pageHtml = '';
1375
		foreach($forms as $urlSegment => $html) {
1376
			$pageHtml .= "<div class=\"params\" id=\"BatchActionParameters_$urlSegment\">$html</div>\n\n";
1377
		}
1378
		return new LiteralField("BatchActionParameters", '<div id="BatchActionParameters" style="display:none">'.$pageHtml.'</div>');
1379
	}
1380
	/**
1381
	 * Returns a list of batch actions
1382
	 */
1383
	public function BatchActionList() {
1384
		return $this->batchactions()->batchActionList();
1385
	}
1386
1387
	public function publishall($request) {
1388
		if(!Permission::check('ADMIN')) return Security::permissionFailure($this);
1389
1390
		increase_time_limit_to();
1391
		increase_memory_limit_to();
1392
1393
		$response = "";
1394
1395
		if(isset($this->requestParams['confirm'])) {
1396
			// Protect against CSRF on destructive action
1397
			if(!SecurityToken::inst()->checkRequest($request)) return $this->httpError(400);
1398
1399
			$start = 0;
1400
			$pages = SiteTree::get()->limit("$start,30");
1401
			$count = 0;
1402
			while($pages) {
1403
				/** @var SiteTree $page */
1404
				foreach($pages as $page) {
1405
					if($page && !$page->canPublish()) {
1406
						return Security::permissionFailure($this);
1407
					}
1408
1409
					$page->publishRecursive();
1410
					$page->destroy();
1411
					unset($page);
1412
					$count++;
1413
					$response .= "<li>$count</li>";
1414
				}
1415
				if($pages->count() > 29) {
1416
					$start += 30;
1417
					$pages = SiteTree::get()->limit("$start,30");
1418
				} else {
1419
					break;
1420
				}
1421
			}
1422
			$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...
1423
1424
		} else {
1425
			$token = SecurityToken::inst();
1426
			$fields = new FieldList();
1427
			$token->updateFieldSet($fields);
1428
			$tokenField = $fields->first();
1429
			$tokenHtml = ($tokenField) ? $tokenField->FieldHolder() : '';
1430
			$publishAllDescription = _t(
1431
				'CMSMain.PUBALLFUN2',
1432
				'Pressing this button will do the equivalent of going to every page and pressing "publish".  '
1433
				. 'It\'s intended to be used after there have been massive edits of the content, such as when '
1434
				. 'the site was first built.'
1435
			);
1436
			$response .= '<h1>' . _t('CMSMain.PUBALLFUN','"Publish All" functionality') . '</h1>
1437
				<p>' . $publishAllDescription . '</p>
1438
				<form method="post" action="publishall">
1439
					<input type="submit" name="confirm" value="'
1440
					. _t('CMSMain.PUBALLCONFIRM',"Please publish every page in the site, copying content stage to live",'Confirmation button') .'" />'
1441
					. $tokenHtml .
1442
				'</form>';
1443
		}
1444
1445
		return $response;
1446
	}
1447
1448
	/**
1449
	 * Restore a completely deleted page from the SiteTree_versions table.
1450
	 *
1451
	 * @param array $data
1452
	 * @param Form $form
1453
	 * @return HTTPResponse
1454
	 */
1455
	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...
1456
		if(!isset($data['ID']) || !is_numeric($data['ID'])) {
1457
			return new HTTPResponse("Please pass an ID in the form content", 400);
1458
		}
1459
1460
		$id = (int)$data['ID'];
1461
		/** @var SiteTree $restoredPage */
1462
		$restoredPage = Versioned::get_latest_version(SiteTree::class, $id);
1463
		if(!$restoredPage) {
1464
			return new HTTPResponse("SiteTree #$id not found", 400);
1465
		}
1466
1467
		$restoredPage = $restoredPage->doRestoreToStage();
1468
1469
		$this->getResponse()->addHeader(
1470
			'X-Status',
1471
			rawurlencode(_t(
1472
				'CMSMain.RESTORED',
1473
				"Restored '{title}' successfully",
1474
				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...
1475
			))
1476
		);
1477
1478
		return $this->getResponseNegotiator()->respond($this->getRequest());
1479
	}
1480
1481
	public function duplicate($request) {
1482
		// Protect against CSRF on destructive action
1483
		if(!SecurityToken::inst()->checkRequest($request)) return $this->httpError(400);
1484
1485
		if(($id = $this->urlParams['ID']) && is_numeric($id)) {
1486
			/** @var SiteTree $page */
1487
			$page = SiteTree::get()->byID($id);
1488 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...
1489
				return Security::permissionFailure($this);
1490
			}
1491
			if(!$page || !$page->ID) throw new HTTPResponse_Exception("Bad record ID #$id", 404);
1492
1493
			$newPage = $page->duplicate();
1494
1495
			// ParentID can be hard-set in the URL.  This is useful for pages with multiple parents
1496
			if(isset($_GET['parentID']) && is_numeric($_GET['parentID'])) {
1497
				$newPage->ParentID = $_GET['parentID'];
1498
				$newPage->write();
1499
			}
1500
1501
			$this->getResponse()->addHeader(
1502
				'X-Status',
1503
				rawurlencode(_t(
1504
					'CMSMain.DUPLICATED',
1505
					"Duplicated '{title}' successfully",
1506
					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...
1507
				))
1508
			);
1509
			$url = Controller::join_links(CMSPageEditController::singleton()->Link('show'), $newPage->ID);
1510
			$this->getResponse()->addHeader('X-ControllerURL', $url);
1511
			$this->getRequest()->addHeader('X-Pjax', 'Content');
1512
			$this->getResponse()->addHeader('X-Pjax', 'Content');
1513
1514
			return $this->getResponseNegotiator()->respond($this->getRequest());
1515
		} else {
1516
			return new HTTPResponse("CMSMain::duplicate() Bad ID: '$id'", 400);
1517
		}
1518
	}
1519
1520
	public function duplicatewithchildren($request) {
1521
		// Protect against CSRF on destructive action
1522
		if(!SecurityToken::inst()->checkRequest($request)) return $this->httpError(400);
1523
		increase_time_limit_to();
1524
		if(($id = $this->urlParams['ID']) && is_numeric($id)) {
1525
			/** @var SiteTree $page */
1526
			$page = SiteTree::get()->byID($id);
1527 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...
1528
				return Security::permissionFailure($this);
1529
			}
1530
			if(!$page || !$page->ID) throw new HTTPResponse_Exception("Bad record ID #$id", 404);
1531
1532
			$newPage = $page->duplicateWithChildren();
1533
1534
			$this->getResponse()->addHeader(
1535
				'X-Status',
1536
				rawurlencode(_t(
1537
					'CMSMain.DUPLICATEDWITHCHILDREN',
1538
					"Duplicated '{title}' and children successfully",
1539
					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...
1540
				))
1541
			);
1542
			$url = Controller::join_links(CMSPageEditController::singleton()->Link('show'), $newPage->ID);
1543
			$this->getResponse()->addHeader('X-ControllerURL', $url);
1544
			$this->getRequest()->addHeader('X-Pjax', 'Content');
1545
			$this->getResponse()->addHeader('X-Pjax', 'Content');
1546
1547
			return $this->getResponseNegotiator()->respond($this->getRequest());
1548
		} else {
1549
			return new HTTPResponse("CMSMain::duplicatewithchildren() Bad ID: '$id'", 400);
1550
		}
1551
	}
1552
1553
	public function providePermissions() {
1554
		$title = CMSPagesController::menu_title();
1555
		return array(
1556
			"CMS_ACCESS_CMSMain" => array(
1557
				'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...
1558
				'category' => _t('Permission.CMS_ACCESS_CATEGORY', 'CMS Access'),
1559
				'help' => _t(
1560
					'CMSMain.ACCESS_HELP',
1561
					'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".'
1562
				),
1563
				'sort' => -99 // below "CMS_ACCESS_LeftAndMain", but above everything else
1564
			)
1565
		);
1566
	}
1567
1568
}
1569