Completed
Pull Request — master (#1617)
by Ingo
02:49
created

CMSMain::init()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 20
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

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

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

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

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

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

Loading history...
724
			} else {
725
				$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...
726
			}
727
728
			// TODO Can't merge $FormAttributes in template at the moment
729
			$form->addExtraClass('center ' . $this->BaseCSSClasses());
730
			// Set validation exemptions for specific actions
731
			$form->setValidationExemptActions(array('restore', 'revert', 'deletefromlive', 'delete', 'unpublish', 'rollback', 'doRollback'));
732
733
			// Announce the capability so the frontend can decide whether to allow preview or not.
734
			if ($record instanceof CMSPreviewable) {
735
				$form->addExtraClass('cms-previewable');
736
			}
737
738
			if(!$record->canEdit() || $deletedFromStage) {
739
				$readonlyFields = $form->Fields()->makeReadonly();
740
				$form->setFields($readonlyFields);
741
			}
742
743
			$form->Fields()->setForm($form);
744
745
			$this->extend('updateEditForm', $form);
746
			return $form;
747
		} else if($id) {
748
			$form = Form::create( $this, "EditForm", new FieldList(
749
				new LabelField('PageDoesntExistLabel',_t('CMSMain.PAGENOTEXISTS',"This page doesn't exist"))), new FieldList()
750
			)->setHTMLID('Form_EditForm');
751
			return $form;
752
		}
753
	}
754
755
	/**
756
	 * @param HTTPRequest $request
757
	 * @return string HTML
758
	 */
759
	public function treeview($request) {
0 ignored issues
show
Unused Code introduced by
The parameter $request is not used and could be removed.

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

Loading history...
760
		return $this->renderWith($this->getTemplatesWithSuffix('_TreeView'));
761
	}
762
763
	/**
764
	 * @param HTTPRequest $request
765
	 * @return string HTML
766
	 */
767
	public function listview($request) {
0 ignored issues
show
Unused Code introduced by
The parameter $request is not used and could be removed.

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

Loading history...
768
		return $this->renderWith($this->getTemplatesWithSuffix('_ListView'));
769
	}
770
771
	/**
772
	 * Callback to request the list of page types allowed under a given page instance.
773
	 * Provides a slower but more precise response over SiteTreeHints
774
	 *
775
	 * @param HTTPRequest $request
776
	 * @return HTTPResponse
777
	 */
778
	public function childfilter($request) {
779
		// Check valid parent specified
780
		$parentID = $request->requestVar('ParentID');
781
		$parent = SiteTree::get()->byID($parentID);
782
		if(!$parent || !$parent->exists()) return $this->httpError(404);
783
784
		// Build hints specific to this class
785
		// Identify disallows and set globals
786
		$classes = SiteTree::page_type_classes();
787
		$disallowedChildren = array();
788
		foreach($classes as $class) {
789
			$obj = singleton($class);
790
			if($obj instanceof HiddenClass) continue;
791
792
			if(!$obj->canCreate(null, array('Parent' => $parent))) {
793
				$disallowedChildren[] = $class;
794
			}
795
		}
796
797
		$this->extend('updateChildFilter', $disallowedChildren, $parentID);
798
		return $this
799
			->getResponse()
800
			->addHeader('Content-Type', 'application/json; charset=utf-8')
801
			->setBody(Convert::raw2json($disallowedChildren));
802
	}
803
804
	/**
805
	 * Safely reconstruct a selected filter from a given set of query parameters
806
	 *
807
	 * @param array $params Query parameters to use
808
	 * @return CMSSiteTreeFilter The filter class, or null if none present
809
	 * @throws InvalidArgumentException if invalid filter class is passed.
810
	 */
811
	protected function getQueryFilter($params) {
812
		if(empty($params['FilterClass'])) return null;
813
		$filterClass = $params['FilterClass'];
814
		if(!is_subclass_of($filterClass, 'SilverStripe\\CMS\\Controllers\\CMSSiteTreeFilter')) {
815
			throw new InvalidArgumentException("Invalid filter class passed: {$filterClass}");
816
		}
817
		return $filterClass::create($params);
818
	}
819
820
	/**
821
	 * Returns the pages meet a certain criteria as {@see CMSSiteTreeFilter} or the subpages of a parent page
822
	 * defaulting to no filter and show all pages in first level.
823
	 * Doubles as search results, if any search parameters are set through {@link SearchForm()}.
824
	 *
825
	 * @param array $params Search filter criteria
826
	 * @param int $parentID Optional parent node to filter on (can't be combined with other search criteria)
827
	 * @return SS_List
828
	 * @throws InvalidArgumentException if invalid filter class is passed.
829
	 */
830
	public function getList($params = array(), $parentID = 0) {
831
		if($filter = $this->getQueryFilter($params)) {
832
			return $filter->getFilteredPages();
833
		} else {
834
			$list = DataList::create($this->stat('tree_class'));
835
			$parentID = is_numeric($parentID) ? $parentID : 0;
836
			return $list->filter("ParentID", $parentID);
837
		}
838
	}
839
840
	/**
841
	 * @return Form
842
	 */
843
	public function ListViewForm() {
844
		$params = $this->getRequest()->requestVar('q');
845
		$list = $this->getList($params, $parentID = $this->getRequest()->requestVar('ParentID'));
846
		$gridFieldConfig = GridFieldConfig::create()->addComponents(
847
			new GridFieldSortableHeader(),
848
			new GridFieldDataColumns(),
849
			new GridFieldPaginator(self::config()->page_length)
850
		);
851
		if($parentID){
852
			$linkSpec = $this->Link();
853
			$linkSpec = $linkSpec . (strstr($linkSpec, '?') ? '&' : '?') . 'ParentID=%d&view=list';
854
			$gridFieldConfig->addComponent(
855
				GridFieldLevelup::create($parentID)
856
					->setLinkSpec($linkSpec)
857
					->setAttributes(array('data-pjax' => 'ListViewForm,Breadcrumbs'))
858
			);
859
		}
860
		$gridField = new GridField('Page','Pages', $list, $gridFieldConfig);
861
		/** @var GridFieldDataColumns $columns */
862
		$columns = $gridField->getConfig()->getComponentByType('SilverStripe\\Forms\\GridField\\GridFieldDataColumns');
863
864
		// Don't allow navigating into children nodes on filtered lists
865
		$fields = array(
866
			'getTreeTitle' => _t('SiteTree.PAGETITLE', 'Page Title'),
867
			'singular_name' => _t('SiteTree.PAGETYPE'),
868
			'LastEdited' => _t('SiteTree.LASTUPDATED', 'Last Updated'),
869
		);
870
		/** @var GridFieldSortableHeader $sortableHeader */
871
		$sortableHeader = $gridField->getConfig()->getComponentByType('SilverStripe\\Forms\\GridField\\GridFieldSortableHeader');
872
		$sortableHeader->setFieldSorting(array('getTreeTitle' => 'Title'));
873
		$gridField->getState()->ParentID = $parentID;
874
875
		if(!$params) {
876
			$fields = array_merge(array('listChildrenLink' => ''), $fields);
877
		}
878
879
		$columns->setDisplayFields($fields);
880
		$columns->setFieldCasting(array(
881
			'Created' => 'DBDatetime->Ago',
882
			'LastEdited' => 'DBDatetime->FormatFromSettings',
883
			'getTreeTitle' => 'HTMLFragment'
884
		));
885
886
		$controller = $this;
887
		$columns->setFieldFormatting(array(
888
			'listChildrenLink' => function($value, &$item) use($controller) {
889
				/** @var SiteTree $item */
890
				$num = $item ? $item->numChildren() : null;
891
				if($num) {
892
					return sprintf(
893
						'<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>',
894
						Controller::join_links(
895
							$controller->Link(),
896
							sprintf("?ParentID=%d&view=list", (int)$item->ID)
897
						),
898
						$num
899
					);
900
				}
901
			},
902
			'getTreeTitle' => function($value, &$item) use($controller) {
903
				return sprintf(
904
					'<a class="action-detail" href="%s">%s</a>',
905
					Controller::join_links(
906
						CMSPageEditController::singleton()->Link('show'),
907
						(int)$item->ID
908
					),
909
					$item->TreeTitle // returns HTML, does its own escaping
910
				);
911
			}
912
		));
913
914
		$negotiator = $this->getResponseNegotiator();
915
		$listview = Form::create(
916
			$this,
917
			'ListViewForm',
918
			new FieldList($gridField),
919
			new FieldList()
920
		)->setHTMLID('Form_ListViewForm');
921
		$listview->setAttribute('data-pjax-fragment', 'ListViewForm');
922 View Code Duplication
		$listview->setValidationResponseCallback(function() use ($negotiator, $listview) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
923
			$request = $this->getRequest();
924
			if($request->isAjax() && $negotiator) {
925
				$listview->setupFormErrors();
926
				$result = $listview->forTemplate();
927
928
				return $negotiator->respond($request, array(
929
					'CurrentForm' => function() use($result) {
930
						return $result;
931
					}
932
				));
933
			}
934
		});
935
936
		$this->extend('updateListView', $listview);
937
938
		$listview->disableSecurityToken();
939
		return $listview;
940
	}
941
942
	public function currentPageID() {
943
		$id = parent::currentPageID();
944
945
		$this->extend('updateCurrentPageID', $id);
946
947
		return $id;
948
	}
949
950
	//------------------------------------------------------------------------------------------//
951
	// Data saving handlers
952
953
	/**
954
	 * Save and Publish page handler
955
	 *
956
	 * @param array $data
957
	 * @param Form $form
958
	 * @return HTTPResponse
959
	 * @throws HTTPResponse_Exception
960
	 */
961
	public function save($data, $form) {
962
		$className = $this->stat('tree_class');
963
964
		// Existing or new record?
965
		$id = $data['ID'];
966
		if(substr($id,0,3) != 'new') {
967
			/** @var SiteTree $record */
968
			$record = DataObject::get_by_id($className, $id);
969
			// Check edit permissions
970
			if($record && !$record->canEdit()) {
971
				return Security::permissionFailure($this);
972
			}
973
			if(!$record || !$record->ID) {
974
				throw new HTTPResponse_Exception("Bad record ID #$id", 404);
975
			}
976
		} else {
977
			if(!$className::singleton()->canCreate()) {
978
				return Security::permissionFailure($this);
979
			}
980
			$record = $this->getNewItem($id, false);
981
		}
982
983
		// Check publishing permissions
984
		$doPublish = !empty($data['publish']);
985
		if($record && $doPublish && !$record->canPublish()) {
986
			return Security::permissionFailure($this);
987
		}
988
989
		// TODO Coupling to SiteTree
990
		$record->HasBrokenLink = 0;
991
		$record->HasBrokenFile = 0;
992
993
		if (!$record->ObsoleteClassName) {
994
			$record->writeWithoutVersion();
995
		}
996
997
		// Update the class instance if necessary
998
		if(isset($data['ClassName']) && $data['ClassName'] != $record->ClassName) {
999
			// Replace $record with a new instance of the new class
1000
			$newClassName = $data['ClassName'];
1001
			$record = $record->newClassInstance($newClassName);
1002
		}
1003
1004
		// save form data into record
1005
		$form->saveInto($record);
1006
		$record->write();
1007
1008
		// If the 'Save & Publish' button was clicked, also publish the page
1009
		if ($doPublish) {
1010
			$record->publishRecursive();
1011
			$message = _t(
1012
				'CMSMain.PUBLISHED',
1013
				"Published '{title}' successfully.",
1014
				['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...
1015
			);
1016
		} else {
1017
			$message = _t(
1018
				'CMSMain.SAVED',
1019
				"Saved '{title}' successfully.",
1020
				['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...
1021
			);
1022
		}
1023
1024
		$this->getResponse()->addHeader('X-Status', rawurlencode($message));
1025
		return $this->getResponseNegotiator()->respond($this->getRequest());
1026
	}
1027
1028
	/**
1029
	 * @uses LeftAndMainExtension->augmentNewSiteTreeItem()
1030
	 *
1031
	 * @param int|string $id
1032
	 * @param bool $setID
1033
	 * @return mixed|DataObject
1034
	 * @throws HTTPResponse_Exception
1035
	 */
1036
	public function getNewItem($id, $setID = true) {
1037
		$parentClass = $this->stat('tree_class');
1038
		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...
1039
1040
		if (!is_a($className, $parentClass, true)) {
1041
			$response = Security::permissionFailure($this);
1042
			if (!$response) {
1043
				$response = $this->getResponse();
1044
			}
1045
			throw new HTTPResponse_Exception($response);
1046
		}
1047
1048
		/** @var SiteTree $newItem */
1049
		$newItem = Injector::inst()->create($className);
1050
		if( !$suffix ) {
1051
			$sessionTag = "NewItems." . $parentID . "." . $className;
1052
			if(Session::get($sessionTag)) {
1053
				$suffix = '-' . Session::get($sessionTag);
1054
				Session::set($sessionTag, Session::get($sessionTag) + 1);
1055
			}
1056
			else
1057
				Session::set($sessionTag, 1);
1058
1059
				$id = $id . $suffix;
1060
		}
1061
1062
		$newItem->Title = _t(
1063
			'CMSMain.NEWPAGE',
1064
			"New {pagetype}",'followed by a page type title',
1065
			array('pagetype' => singleton($className)->i18n_singular_name())
0 ignored issues
show
Documentation introduced by
array('pagetype' => sing...->i18n_singular_name()) is of type array<string,?,{"pagetype":"?"}>, but the function expects a string.

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1066
		);
1067
		$newItem->ClassName = $className;
1068
		$newItem->ParentID = $parentID;
1069
1070
		// DataObject::fieldExists only checks the current class, not the hierarchy
1071
		// This allows the CMS to set the correct sort value
1072
		if($newItem->castingHelper('Sort')) {
1073
			$newItem->Sort = DB::prepared_query('SELECT MAX("Sort") FROM "SiteTree" WHERE "ParentID" = ?', array($parentID))->value() + 1;
1074
		}
1075
1076
		if($setID) $newItem->ID = $id;
1077
1078
		# Some modules like subsites add extra fields that need to be set when the new item is created
1079
		$this->extend('augmentNewSiteTreeItem', $newItem);
1080
1081
		return $newItem;
1082
	}
1083
1084
	/**
1085
	 * Actually perform the publication step
1086
	 *
1087
	 * @param Versioned|DataObject $record
1088
	 * @return mixed
1089
	 */
1090
	public function performPublish($record) {
1091
		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...
1092
			return Security::permissionFailure($this);
1093
		}
1094
1095
		$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...
1096
	}
1097
1098
	/**
1099
	 * Reverts a page by publishing it to live.
1100
	 * Use {@link restorepage()} if you want to restore a page
1101
	 * which was deleted from draft without publishing.
1102
	 *
1103
	 * @uses SiteTree->doRevertToLive()
1104
	 *
1105
	 * @param array $data
1106
	 * @param Form $form
1107
	 * @return HTTPResponse
1108
	 * @throws HTTPResponse_Exception
1109
	 */
1110
	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...
1111
		if(!isset($data['ID'])) {
1112
			throw new HTTPResponse_Exception("Please pass an ID in the form content", 400);
1113
		}
1114
1115
		$id = (int) $data['ID'];
1116
		$restoredPage = Versioned::get_latest_version("SilverStripe\\CMS\\Model\\SiteTree", $id);
1117
		if(!$restoredPage) {
1118
			throw new HTTPResponse_Exception("SiteTree #$id not found", 400);
1119
		}
1120
1121
		/** @var SiteTree $record */
1122
		$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...
1123
			'"SiteTree_Live"."ID"' => $id
1124
		));
1125
1126
		// a user can restore a page without publication rights, as it just adds a new draft state
1127
		// (this action should just be available when page has been "deleted from draft")
1128
		if($record && !$record->canEdit()) {
1129
			return Security::permissionFailure($this);
1130
		}
1131
		if(!$record || !$record->ID) {
1132
			throw new HTTPResponse_Exception("Bad record ID #$id", 404);
1133
		}
1134
1135
		$record->doRevertToLive();
1136
1137
		$this->getResponse()->addHeader(
1138
			'X-Status',
1139
			rawurlencode(_t(
1140
				'CMSMain.RESTORED',
1141
				"Restored '{title}' successfully",
1142
				'Param %s is a title',
1143
				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...
1144
			))
1145
		);
1146
1147
		return $this->getResponseNegotiator()->respond($this->getRequest());
1148
	}
1149
1150
	/**
1151
	 * Delete the current page from draft stage.
1152
	 *
1153
	 * @see deletefromlive()
1154
	 *
1155
	 * @param array $data
1156
	 * @param Form $form
1157
	 * @return HTTPResponse
1158
	 * @throws HTTPResponse_Exception
1159
	 */
1160 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...
1161
		$id = $data['ID'];
1162
		$record = SiteTree::get()->byID($id);
1163
		if($record && !$record->canDelete()) {
1164
			return Security::permissionFailure();
1165
		}
1166
		if(!$record || !$record->ID) {
1167
			throw new HTTPResponse_Exception("Bad record ID #$id", 404);
1168
		}
1169
1170
		// Delete record
1171
		$record->delete();
1172
1173
		$this->getResponse()->addHeader(
1174
			'X-Status',
1175
			rawurlencode(sprintf(_t('CMSMain.REMOVEDPAGEFROMDRAFT',"Removed '%s' from the draft site"), $record->Title))
1176
		);
1177
1178
		// Even if the record has been deleted from stage and live, it can be viewed in "archive mode"
1179
		return $this->getResponseNegotiator()->respond($this->getRequest());
1180
	}
1181
1182
	/**
1183
	 * Delete this page from both live and stage
1184
	 *
1185
	 * @param array $data
1186
	 * @param Form $form
1187
	 * @return HTTPResponse
1188
	 * @throws HTTPResponse_Exception
1189
	 */
1190 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...
1191
		$id = $data['ID'];
1192
		/** @var SiteTree $record */
1193
		$record = SiteTree::get()->byID($id);
1194
		if(!$record || !$record->exists()) {
1195
			throw new HTTPResponse_Exception("Bad record ID #$id", 404);
1196
		}
1197
		if(!$record->canArchive()) {
1198
			return Security::permissionFailure();
1199
		}
1200
1201
		// Archive record
1202
		$record->doArchive();
1203
1204
		$this->getResponse()->addHeader(
1205
			'X-Status',
1206
			rawurlencode(sprintf(_t('CMSMain.ARCHIVEDPAGE',"Archived page '%s'"), $record->Title))
1207
		);
1208
1209
		// Even if the record has been deleted from stage and live, it can be viewed in "archive mode"
1210
		return $this->getResponseNegotiator()->respond($this->getRequest());
1211
	}
1212
1213
	public function publish($data, $form) {
1214
		$data['publish'] = '1';
1215
1216
		return $this->save($data, $form);
1217
	}
1218
1219
	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...
1220
		$className = $this->stat('tree_class');
1221
		/** @var SiteTree $record */
1222
		$record = DataObject::get_by_id($className, $data['ID']);
1223
1224
		if($record && !$record->canUnpublish()) {
1225
			return Security::permissionFailure($this);
1226
		}
1227
		if(!$record || !$record->ID) {
1228
			throw new HTTPResponse_Exception("Bad record ID #" . (int)$data['ID'], 404);
1229
		}
1230
1231
		$record->doUnpublish();
1232
1233
		$this->getResponse()->addHeader(
1234
			'X-Status',
1235
			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...
1236
		);
1237
1238
		return $this->getResponseNegotiator()->respond($this->getRequest());
1239
	}
1240
1241
	/**
1242
	 * @return HTTPResponse
1243
	 */
1244
	public function rollback() {
1245
		return $this->doRollback(array(
1246
			'ID' => $this->currentPageID(),
1247
			'Version' => $this->getRequest()->param('VersionID')
1248
		), 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...
1249
	}
1250
1251
	/**
1252
	 * Rolls a site back to a given version ID
1253
	 *
1254
	 * @param array $data
1255
	 * @param Form $form
1256
	 * @return HTTPResponse
1257
	 */
1258
	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...
1259
		$this->extend('onBeforeRollback', $data['ID']);
1260
1261
		$id = (isset($data['ID'])) ? (int) $data['ID'] : null;
1262
		$version = (isset($data['Version'])) ? (int) $data['Version'] : null;
1263
1264
		/** @var DataObject|Versioned $record */
1265
		$record = DataObject::get_by_id($this->stat('tree_class'), $id);
1266
		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...
1267
			return Security::permissionFailure($this);
1268
		}
1269
1270
		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...
1271
			$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...
1272
			$message = _t(
1273
				'CMSMain.ROLLEDBACKVERSIONv2',
1274
				"Rolled back to version #%d.",
1275
				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...
1276
			);
1277
		} else {
1278
			$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...
1279
			$message = _t(
1280
				'CMSMain.ROLLEDBACKPUBv2',"Rolled back to published version."
1281
			);
1282
		}
1283
1284
		$this->getResponse()->addHeader('X-Status', rawurlencode($message));
1285
1286
		// Can be used in different contexts: In normal page edit view, in which case the redirect won't have any effect.
1287
		// Or in history view, in which case a revert causes the CMS to re-load the edit view.
1288
		// The X-Pjax header forces a "full" content refresh on redirect.
1289
		$url = Controller::join_links(CMSPageEditController::singleton()->Link('show'), $record->ID);
1290
		$this->getResponse()->addHeader('X-ControllerURL', $url);
1291
		$this->getRequest()->addHeader('X-Pjax', 'Content');
1292
		$this->getResponse()->addHeader('X-Pjax', 'Content');
1293
1294
		return $this->getResponseNegotiator()->respond($this->getRequest());
1295
	}
1296
1297
	/**
1298
	 * Batch Actions Handler
1299
	 */
1300
	public function batchactions() {
1301
		return new CMSBatchActionHandler($this, 'batchactions');
1302
	}
1303
1304
	public function BatchActionParameters() {
1305
		$batchActions = CMSBatchActionHandler::config()->batch_actions;
1306
1307
		$forms = array();
1308
		foreach($batchActions as $urlSegment => $batchAction) {
1309
			$SNG_action = singleton($batchAction);
1310
			if ($SNG_action->canView() && $fieldset = $SNG_action->getParameterFields()) {
1311
				$formHtml = '';
1312
				/** @var FormField $field */
1313
				foreach($fieldset as $field) {
1314
					$formHtml .= $field->Field();
1315
				}
1316
				$forms[$urlSegment] = $formHtml;
1317
			}
1318
		}
1319
		$pageHtml = '';
1320
		foreach($forms as $urlSegment => $html) {
1321
			$pageHtml .= "<div class=\"params\" id=\"BatchActionParameters_$urlSegment\">$html</div>\n\n";
1322
		}
1323
		return new LiteralField("BatchActionParameters", '<div id="BatchActionParameters" style="display:none">'.$pageHtml.'</div>');
1324
	}
1325
	/**
1326
	 * Returns a list of batch actions
1327
	 */
1328
	public function BatchActionList() {
1329
		return $this->batchactions()->batchActionList();
1330
	}
1331
1332
	public function publishall($request) {
1333
		if(!Permission::check('ADMIN')) return Security::permissionFailure($this);
1334
1335
		increase_time_limit_to();
1336
		increase_memory_limit_to();
1337
1338
		$response = "";
1339
1340
		if(isset($this->requestParams['confirm'])) {
1341
			// Protect against CSRF on destructive action
1342
			if(!SecurityToken::inst()->checkRequest($request)) return $this->httpError(400);
1343
1344
			$start = 0;
1345
			$pages = SiteTree::get()->limit("$start,30");
1346
			$count = 0;
1347
			while($pages) {
1348
				/** @var SiteTree $page */
1349
				foreach($pages as $page) {
1350
					if($page && !$page->canPublish()) {
1351
						return Security::permissionFailure($this);
1352
					}
1353
1354
					$page->publishRecursive();
1355
					$page->destroy();
1356
					unset($page);
1357
					$count++;
1358
					$response .= "<li>$count</li>";
1359
				}
1360
				if($pages->count() > 29) {
1361
					$start += 30;
1362
					$pages = SiteTree::get()->limit("$start,30");
1363
				} else {
1364
					break;
1365
				}
1366
			}
1367
			$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...
1368
1369
		} else {
1370
			$token = SecurityToken::inst();
1371
			$fields = new FieldList();
1372
			$token->updateFieldSet($fields);
1373
			$tokenField = $fields->first();
1374
			$tokenHtml = ($tokenField) ? $tokenField->FieldHolder() : '';
1375
			$response .= '<h1>' . _t('CMSMain.PUBALLFUN','"Publish All" functionality') . '</h1>
1376
				<p>' . _t('CMSMain.PUBALLFUN2', 'Pressing this button will do the equivalent of going to every page and pressing "publish".  It\'s
1377
				intended to be used after there have been massive edits of the content, such as when the site was
1378
				first built.') . '</p>
1379
				<form method="post" action="publishall">
1380
					<input type="submit" name="confirm" value="'
1381
					. _t('CMSMain.PUBALLCONFIRM',"Please publish every page in the site, copying content stage to live",'Confirmation button') .'" />'
1382
					. $tokenHtml .
1383
				'</form>';
1384
		}
1385
1386
		return $response;
1387
	}
1388
1389
	/**
1390
	 * Restore a completely deleted page from the SiteTree_versions table.
1391
	 *
1392
	 * @param array $data
1393
	 * @param Form $form
1394
	 * @return HTTPResponse
1395
	 */
1396
	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...
1397
		if(!isset($data['ID']) || !is_numeric($data['ID'])) {
1398
			return new HTTPResponse("Please pass an ID in the form content", 400);
1399
		}
1400
1401
		$id = (int)$data['ID'];
1402
		/** @var SiteTree $restoredPage */
1403
		$restoredPage = Versioned::get_latest_version("SilverStripe\\CMS\\Model\\SiteTree", $id);
1404
		if(!$restoredPage) {
1405
			return new HTTPResponse("SiteTree #$id not found", 400);
1406
		}
1407
1408
		$restoredPage = $restoredPage->doRestoreToStage();
1409
1410
		$this->getResponse()->addHeader(
1411
			'X-Status',
1412
			rawurlencode(_t(
1413
				'CMSMain.RESTORED',
1414
				"Restored '{title}' successfully",
1415
				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...
1416
			))
1417
		);
1418
1419
		return $this->getResponseNegotiator()->respond($this->getRequest());
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $this->getRespons...d($this->getRequest()); (SS_HTTPResponse) is incompatible with the return type documented by SilverStripe\CMS\Controllers\CMSMain::restore of type SilverStripe\Control\HTTPResponse.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
1420
	}
1421
1422
	public function duplicate($request) {
1423
		// Protect against CSRF on destructive action
1424
		if(!SecurityToken::inst()->checkRequest($request)) return $this->httpError(400);
1425
1426
		if(($id = $this->urlParams['ID']) && is_numeric($id)) {
1427
			/** @var SiteTree $page */
1428
			$page = SiteTree::get()->byID($id);
1429 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...
1430
				return Security::permissionFailure($this);
1431
			}
1432
			if(!$page || !$page->ID) throw new HTTPResponse_Exception("Bad record ID #$id", 404);
1433
1434
			$newPage = $page->duplicate();
1435
1436
			// ParentID can be hard-set in the URL.  This is useful for pages with multiple parents
1437
			if(isset($_GET['parentID']) && is_numeric($_GET['parentID'])) {
1438
				$newPage->ParentID = $_GET['parentID'];
1439
				$newPage->write();
1440
			}
1441
1442
			$this->getResponse()->addHeader(
1443
				'X-Status',
1444
				rawurlencode(_t(
1445
					'CMSMain.DUPLICATED',
1446
					"Duplicated '{title}' successfully",
1447
					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...
1448
				))
1449
			);
1450
			$url = Controller::join_links(CMSPageEditController::singleton()->Link('show'), $newPage->ID);
1451
			$this->getResponse()->addHeader('X-ControllerURL', $url);
1452
			$this->getRequest()->addHeader('X-Pjax', 'Content');
1453
			$this->getResponse()->addHeader('X-Pjax', 'Content');
1454
1455
			return $this->getResponseNegotiator()->respond($this->getRequest());
1456
		} else {
1457
			return new HTTPResponse("CMSMain::duplicate() Bad ID: '$id'", 400);
1458
		}
1459
	}
1460
1461
	public function duplicatewithchildren($request) {
1462
		// Protect against CSRF on destructive action
1463
		if(!SecurityToken::inst()->checkRequest($request)) return $this->httpError(400);
1464
		increase_time_limit_to();
1465
		if(($id = $this->urlParams['ID']) && is_numeric($id)) {
1466
			/** @var SiteTree $page */
1467
			$page = SiteTree::get()->byID($id);
1468 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...
1469
				return Security::permissionFailure($this);
1470
			}
1471
			if(!$page || !$page->ID) throw new HTTPResponse_Exception("Bad record ID #$id", 404);
1472
1473
			$newPage = $page->duplicateWithChildren();
1474
1475
			$this->getResponse()->addHeader(
1476
				'X-Status',
1477
				rawurlencode(_t(
1478
					'CMSMain.DUPLICATEDWITHCHILDREN',
1479
					"Duplicated '{title}' and children successfully",
1480
					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...
1481
				))
1482
			);
1483
			$url = Controller::join_links(CMSPageEditController::singleton()->Link('show'), $newPage->ID);
1484
			$this->getResponse()->addHeader('X-ControllerURL', $url);
1485
			$this->getRequest()->addHeader('X-Pjax', 'Content');
1486
			$this->getResponse()->addHeader('X-Pjax', 'Content');
1487
1488
			return $this->getResponseNegotiator()->respond($this->getRequest());
1489
		} else {
1490
			return new HTTPResponse("CMSMain::duplicatewithchildren() Bad ID: '$id'", 400);
1491
		}
1492
	}
1493
1494
	public function providePermissions() {
1495
		$title = CMSPagesController::menu_title();
1496
		return array(
1497
			"CMS_ACCESS_CMSMain" => array(
1498
				'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...
1499
				'category' => _t('Permission.CMS_ACCESS_CATEGORY', 'CMS Access'),
1500
				'help' => _t(
1501
					'CMSMain.ACCESS_HELP',
1502
					'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".'
1503
				),
1504
				'sort' => -99 // below "CMS_ACCESS_LeftAndMain", but above everything else
1505
			)
1506
		);
1507
	}
1508
1509
}
1510