Completed
Push — webpack ( 1af445...79be2b )
by Damian
10:03
created

LeftAndMain   F

Complexity

Total Complexity 294

Size/Duplication

Total Lines 1992
Duplicated Lines 2.51 %

Coupling/Cohesion

Components 1
Dependencies 47

Importance

Changes 0
Metric Value
wmc 294
lcom 1
cbo 47
dl 50
loc 1992
rs 0.5217
c 0
b 0
f 0

62 Methods

Rating   Name   Duplication   Size   Complexity  
B handleRequest() 0 26 4
B redirect() 0 22 5
A index() 0 3 1
A ShowSwitchView() 0 3 1
A Link() 0 17 2
A menu_title_for_class() 0 4 1
B menu_title() 0 21 6
A show() 0 5 2
A Menu() 0 3 1
A MenuCurrentItem() 0 4 1
A Content() 0 3 1
A getClientConfig() 0 7 1
A getSchemaResponse() 0 10 2
B getSchemaForForm() 0 28 4
A getCombinedClientConfig() 0 23 2
B schema() 0 24 4
D canView() 0 32 10
A getRequiredPermissions() 0 11 3
F init() 20 163 29
A menu_icon_for_class() 0 8 2
B getResponseNegotiator() 0 26 2
D MainMenu() 0 81 19
A getTemplatesWithSuffix() 0 4 1
A PreviewPanel() 0 7 2
B getRecord() 0 12 7
D Breadcrumbs() 0 29 10
A SiteTreeAsUL() 0 5 1
A getSearchFilter() 0 16 3
F getSiteTreeFor() 0 133 27
A getsubtree() 0 16 1
C updatetreenodes() 0 44 8
D save() 0 42 9
A getNewItem() 0 8 2
B delete() 0 16 5
D savetreenode() 0 97 21
A CanOrganiseSitetree() 0 3 3
A EditForm() 0 3 1
F getEditForm() 12 135 28
B EmptyForm() 0 28 1
A EditorToolbar() 0 3 1
A Tools() 9 9 2
A EditFormTools() 9 9 2
A batchactions() 0 3 1
B BatchActionsForm() 0 31 2
A printable() 0 13 2
A getSilverStripeNavigator() 0 8 2
B currentPageID() 0 14 8
A setCurrentPageID() 0 4 1
A currentPage() 0 3 1
A isCurrentPage() 0 3 1
A sessionNamespace() 0 4 2
A LinkPreview() 0 3 1
C CMSVersion() 0 59 15
A SwitchView() 0 6 2
A SiteConfig() 0 3 2
A ApplicationLink() 0 3 1
A getApplicationName() 0 3 1
A Title() 0 5 2
A SectionTitle() 0 12 4
A BaseCSSClasses() 0 3 1
A Locale() 0 3 1
B providePermissions() 0 45 6

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like LeftAndMain often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use LeftAndMain, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace SilverStripe\Admin;
4
5
use SilverStripe\CMS\Controllers\CMSPageEditController;
6
use SilverStripe\CMS\Controllers\CMSPagesController;
7
use SilverStripe\CMS\Model\SiteTree;
8
use SilverStripe\CMS\Model\VirtualPage;
9
use SilverStripe\CMS\Controllers\SilverStripeNavigator;
10
use SilverStripe\Control\ContentNegotiator;
11
use SilverStripe\Control\Director;
12
use SilverStripe\Control\HTTPResponse;
13
use SilverStripe\Control\Session;
14
use SilverStripe\Control\HTTPRequest;
15
use SilverStripe\Control\HTTPResponse_Exception;
16
use SilverStripe\Control\Controller;
17
use SilverStripe\Control\PjaxResponseNegotiator;
18
use SilverStripe\Core\Convert;
19
use SilverStripe\Core\Config\Config;
20
use SilverStripe\Core\Cache;
21
use SilverStripe\Core\ClassInfo;
22
use SilverStripe\Core\Injector\Injector;
23
use SilverStripe\Dev\Deprecation;
24
use SilverStripe\Forms\Form;
25
use SilverStripe\Forms\HiddenField;
26
use SilverStripe\Forms\LiteralField;
27
use SilverStripe\Forms\FormAction;
28
use SilverStripe\Forms\FieldList;
29
use SilverStripe\Forms\DropdownField;
30
use SilverStripe\Forms\PrintableTransformation;
31
use SilverStripe\Forms\HTMLEditor\HTMLEditorConfig;
32
use SilverStripe\Forms\HTMLEditor\HTMLEditorField_Toolbar;
33
use SilverStripe\Forms\Schema\FormSchema;
34
use SilverStripe\i18n\i18n;
35
use SilverStripe\ORM\FieldType\DBHTMLText;
36
use SilverStripe\ORM\SS_List;
37
use SilverStripe\ORM\Versioning\Versioned;
38
use SilverStripe\ORM\DataModel;
39
use SilverStripe\ORM\ValidationException;
40
use SilverStripe\ORM\ArrayList;
41
use SilverStripe\ORM\FieldType\DBField;
42
use SilverStripe\ORM\DataObject;
43
use SilverStripe\ORM\DB;
44
use SilverStripe\Security\SecurityToken;
45
use SilverStripe\Security\Member;
46
use SilverStripe\Security\Permission;
47
use SilverStripe\Security\Security;
48
use SilverStripe\Security\PermissionProvider;
49
use SilverStripe\View\SSViewer;
50
use SilverStripe\View\Requirements;
51
use SilverStripe\View\ArrayData;
52
use ReflectionClass;
53
use InvalidArgumentException;
54
55
use SilverStripe\SiteConfig\SiteConfig;
56
57
58
/**
59
 * LeftAndMain is the parent class of all the two-pane views in the CMS.
60
 * If you are wanting to add more areas to the CMS, you can do it by subclassing LeftAndMain.
61
 *
62
 * This is essentially an abstract class which should be subclassed.
63
 * See {@link CMSMain} for a good example.
64
 *
65
 * @property FormSchema $schema
66
 */
67
class LeftAndMain extends Controller implements PermissionProvider {
68
69
	/**
70
	 * Enable front-end debugging (increases verbosity) in dev mode.
71
	 * Will be ignored in live environments.
72
	 *
73
	 * @var bool
74
	 */
75
	private static $client_debugging = true;
76
77
	/**
78
	 * The current url segment attached to the LeftAndMain instance
79
	 *
80
	 * @config
81
	 * @var string
82
	 */
83
	private static $url_segment;
84
85
	/**
86
	 * @config
87
	 * @var string
88
	 */
89
	private static $url_rule = '/$Action/$ID/$OtherID';
90
91
	/**
92
	 * @config
93
	 * @var string
94
	 */
95
	private static $menu_title;
96
97
	/**
98
	 * @config
99
	 * @var string
100
	 */
101
	private static $menu_icon;
102
103
	/**
104
	 * @config
105
	 * @var int
106
	 */
107
	private static $menu_priority = 0;
108
109
	/**
110
	 * @config
111
	 * @var int
112
	 */
113
	private static $url_priority = 50;
114
115
	/**
116
	 * A subclass of {@link DataObject}.
117
	 *
118
	 * Determines what is managed in this interface, through
119
	 * {@link getEditForm()} and other logic.
120
	 *
121
	 * @config
122
	 * @var string
123
	 */
124
	private static $tree_class = null;
125
126
	/**
127
	 * The url used for the link in the Help tab in the backend
128
	 *
129
	 * @config
130
	 * @var string
131
	 */
132
	private static $help_link = '//userhelp.silverstripe.org/framework/en/3.3';
133
134
	/**
135
	 * @var array
136
	 */
137
	private static $allowed_actions = [
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...
138
		'index',
139
		'save',
140
		'savetreenode',
141
		'getsubtree',
142
		'updatetreenodes',
143
		'printable',
144
		'show',
145
		'EditorToolbar',
146
		'EditForm',
147
		'AddForm',
148
		'batchactions',
149
		'BatchActionsForm',
150
		'schema',
151
	];
152
153
	private static $url_handlers = [
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...
154
		'GET schema/$FormName/$ItemID' => 'schema'
155
	];
156
157
	private static $dependencies = [
158
		'schema' => '%$FormSchema'
159
	];
160
161
	/**
162
	 * Assign themes to use for cms
163
	 *
164
	 * @config
165
	 * @var array
166
	 */
167
	private static $admin_themes = [
168
		'/framework/admin/themes/cms-forms',
169
		SSViewer::DEFAULT_THEME,
170
	];
171
172
	/**
173
	 * Codes which are required from the current user to view this controller.
174
	 * If multiple codes are provided, all of them are required.
175
	 * All CMS controllers require "CMS_ACCESS_LeftAndMain" as a baseline check,
176
	 * and fall back to "CMS_ACCESS_<class>" if no permissions are defined here.
177
	 * See {@link canView()} for more details on permission checks.
178
	 *
179
	 * @config
180
	 * @var array
181
	 */
182
	private static $required_permission_codes;
183
184
	/**
185
	 * @config
186
	 * @var String Namespace for session info, e.g. current record.
187
	 * Defaults to the current class name, but can be amended to share a namespace in case
188
	 * controllers are logically bundled together, and mainly separated
189
	 * to achieve more flexible templating.
190
	 */
191
	private static $session_namespace;
192
193
	/**
194
	 * Register additional requirements through the {@link Requirements} class.
195
	 * Used mainly to work around the missing "lazy loading" functionality
196
	 * for getting css/javascript required after an ajax-call (e.g. loading the editform).
197
	 *
198
	 * YAML configuration example:
199
	 * <code>
200
	 * LeftAndMain:
201
	 *   extra_requirements_javascript:
202
	 *     mysite/javascript/myscript.js:
203
	 * </code>
204
	 *
205
	 * @config
206
	 * @var array
207
	 */
208
	private static $extra_requirements_javascript = array();
209
210
	/**
211
	 * YAML configuration example:
212
	 * <code>
213
	 * LeftAndMain:
214
	 *   extra_requirements_css:
215
	 *     mysite/css/mystyle.css:
216
	 *       media: screen
217
	 * </code>
218
	 *
219
	 * @config
220
	 * @var array See {@link extra_requirements_javascript}
221
	 */
222
	private static $extra_requirements_css = array();
223
224
	/**
225
	 * @config
226
	 * @var array See {@link extra_requirements_javascript}
227
	 */
228
	private static $extra_requirements_themedCss = array();
229
230
	/**
231
	 * If true, call a keepalive ping every 5 minutes from the CMS interface,
232
	 * to ensure that the session never dies.
233
	 *
234
	 * @config
235
	 * @var boolean
236
	 */
237
	private static $session_keepalive_ping = true;
238
239
	/**
240
	 * Value of X-Frame-Options header
241
	 *
242
	 * @config
243
	 * @var string
244
	 */
245
	private static $frame_options = 'SAMEORIGIN';
246
247
	/**
248
	 * @var PjaxResponseNegotiator
249
	 */
250
	protected $responseNegotiator;
251
252
	/**
253
	 * Gets the combined configuration of all LeafAndMain subclasses required by the client app.
254
	 *
255
	 * @return array
256
	 *
257
	 * WARNING: Experimental API
258
	 */
259
	public function getCombinedClientConfig() {
260
		$combinedClientConfig = ['sections' => []];
261
		$cmsClassNames = CMSMenu::get_cms_classes('SilverStripe\\Admin\\LeftAndMain', true, CMSMenu::URL_PRIORITY);
262
263
		foreach ($cmsClassNames as $className) {
264
			$combinedClientConfig['sections'][$className] =  Injector::inst()->get($className)->getClientConfig();
265
		}
266
267
		// Pass in base url (absolute and relative)
268
		$combinedClientConfig['baseUrl'] = Director::baseURL();
269
		$combinedClientConfig['absoluteBaseUrl'] = Director::absoluteBaseURL();
270
        $combinedClientConfig['adminUrl'] = AdminRootController::admin_url();
271
272
		// Get "global" CSRF token for use in JavaScript
273
		$token = SecurityToken::inst();
274
		$combinedClientConfig[$token->getName()] = $token->getValue();
275
276
		// Set env
277
		$combinedClientConfig['environment'] = Director::get_environment_type();
278
		$combinedClientConfig['debugging'] = $this->config()->client_debugging;
0 ignored issues
show
Documentation introduced by
The property client_debugging does not exist on object<SilverStripe\Core\Config\Config_ForClass>. 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...
279
280
		return Convert::raw2json($combinedClientConfig);
281
	}
282
283
	/**
284
	 * Returns configuration required by the client app.
285
	 *
286
	 * @return array
287
	 *
288
	 * WARNING: Experimental API
289
	 */
290
	public function getClientConfig() {
291
		return [
292
			// Trim leading/trailing slash to make it easier to concatenate URL
293
			// and use in routing definitions.
294
			'url' => trim($this->Link(), '/'),
295
		];
296
	}
297
298
	/**
299
	 * Gets a JSON schema representing the current edit form.
300
	 *
301
	 * WARNING: Experimental API.
302
	 *
303
	 * @param HTTPRequest $request
304
	 * @return HTTPResponse
305
	 */
306
	public function schema($request) {
307
		$response = $this->getResponse();
308
		$formName = $request->param('FormName');
309
		$itemID = $request->param('ItemID');
310
311
		if (!$formName) {
312
			return (new HTTPResponse('Missing request params', 400));
313
		}
314
315
		if(!$this->hasMethod("get{$formName}")) {
316
			return (new HTTPResponse('Form not found', 404));
317
		}
318
319
		if(!$this->hasAction($formName)) {
320
			return (new HTTPResponse('Form not accessible', 401));
321
		}
322
323
		$form = $this->{"get{$formName}"}($itemID);
324
325
		$response->addHeader('Content-Type', 'application/json');
326
		$response->setBody(Convert::raw2json($this->getSchemaForForm($form)));
327
328
		return $response;
329
	}
330
331
	/**
332
	 * Given a form, generate a response containing the requested form
333
	 * schema if X-Formschema-Request header is set.
334
	 *
335
	 * @param Form $form
336
	 * @return HTTPResponse
337
	 */
338
	protected function getSchemaResponse($form) {
339
		$request = $this->getRequest();
340
		if($request->getHeader('X-Formschema-Request')) {
341
			$data = $this->getSchemaForForm($form);
342
			$response = new HTTPResponse(Convert::raw2json($data));
343
			$response->addHeader('Content-Type', 'application/json');
344
			return $response;
345
		}
346
		return null;
347
	}
348
349
	/**
350
	 * Returns a representation of the provided {@link Form} as structured data,
351
	 * based on the request data.
352
	 *
353
	 * @param Form $form
354
	 * @return array
355
	 */
356
	protected function getSchemaForForm(Form $form) {
357
		$request = $this->getRequest();
358
		$return = null;
0 ignored issues
show
Unused Code introduced by
$return 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...
359
360
		// Valid values for the "X-Formschema-Request" header are "schema" and "state".
361
		// If either of these values are set they will be stored in the $schemaParst array
362
		// and used to construct the response body.
363
		if ($schemaHeader = $request->getHeader('X-Formschema-Request')) {
364
			$schemaParts = array_filter(explode(',', $schemaHeader), function($value) {
365
				$validHeaderValues = ['schema', 'state'];
366
				return in_array(trim($value), $validHeaderValues);
367
			});
368
		} else {
369
			$schemaParts = ['schema'];
370
		}
371
372
		$return = ['id' => $form->FormName()];
373
374
		if (in_array('schema', $schemaParts)) {
375
			$return['schema'] = $this->schema->getSchema($form);
376
		}
377
378
		if (in_array('state', $schemaParts)) {
379
			$return['state'] = $this->schema->getState($form);
380
		}
381
382
		return $return;
383
	}
384
385
	/**
386
	 * @param Member $member
387
	 * @return boolean
388
	 */
389
	public function canView($member = null) {
390
		if(!$member && $member !== FALSE) $member = Member::currentUser();
391
392
		// cms menus only for logged-in members
393
		if(!$member) return false;
394
395
		// alternative extended checks
396
		if($this->hasMethod('alternateAccessCheck')) {
397
			$alternateAllowed = $this->alternateAccessCheck();
398
			if($alternateAllowed === false) {
399
				return false;
400
			}
401
		}
402
403
		// Check for "CMS admin" permission
404
		if(Permission::checkMember($member, "CMS_ACCESS_LeftAndMain")) {
405
			return true;
406
		}
407
408
		// Check for LeftAndMain sub-class permissions
409
		$codes = $this->getRequiredPermissions();
410
		if($codes === false) { // allow explicit FALSE to disable subclass check
411
			return true;
412
		}
413
		foreach((array)$codes as $code) {
414
			if(!Permission::checkMember($member, $code)) {
415
				return false;
416
			}
417
		}
418
419
		return true;
420
	}
421
422
	/**
423
	 * Get list of required permissions
424
	 *
425
	 * @return array|string|bool Code, array of codes, or false if no permission required
426
	 */
427
	public static function getRequiredPermissions() {
428
		$class = get_called_class();
429
		$code = Config::inst()->get($class, 'required_permission_codes', Config::FIRST_SET);
430
		if ($code === false) {
431
			return false;
432
		}
433
		if ($code) {
434
			return $code;
435
		}
436
		return "CMS_ACCESS_" . $class;
437
	}
438
439
	/**
440
	 * @uses LeftAndMainExtension->init()
441
	 * @uses LeftAndMainExtension->accessedCMS()
442
	 * @uses CMSMenu
443
	 */
444
	protected function init() {
445
		parent::init();
446
447
		SSViewer::config()->update('rewrite_hash_links', false);
448
		ContentNegotiator::config()->update('enabled', false);
449
450
		// set language
451
		$member = Member::currentUser();
452
		if(!empty($member->Locale)) {
453
			i18n::set_locale($member->Locale);
454
		}
455
		if(!empty($member->DateFormat)) {
456
			i18n::config()->date_format = $member->DateFormat;
0 ignored issues
show
Documentation introduced by
The property date_format does not exist on object<SilverStripe\Core\Config\Config_ForClass>. 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...
457
		}
458
		if(!empty($member->TimeFormat)) {
459
			i18n::config()->time_format = $member->TimeFormat;
0 ignored issues
show
Documentation introduced by
The property time_format does not exist on object<SilverStripe\Core\Config\Config_ForClass>. 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...
460
		}
461
462
		// can't be done in cms/_config.php as locale is not set yet
463
		CMSMenu::add_link(
464
			'Help',
465
			_t('LeftAndMain.HELP', 'Help', 'Menu title'),
466
			$this->config()->help_link,
467
			-2,
468
			array(
469
				'target' => '_blank'
470
			)
471
		);
472
473
		// Allow customisation of the access check by a extension
474
		// Also all the canView() check to execute Controller::redirect()
475
		if(!$this->canView() && !$this->getResponse()->isFinished()) {
476
			// When access /admin/, we should try a redirect to another part of the admin rather than be locked out
477
			$menu = $this->MainMenu();
478
			foreach($menu as $candidate) {
479
				if(
480
					$candidate->Link &&
481
					$candidate->Link != $this->Link()
482
					&& $candidate->MenuItem->controller
483
					&& singleton($candidate->MenuItem->controller)->canView()
484
				) {
485
					$this->redirect($candidate->Link);
486
					return;
487
				}
488
			}
489
490
			if(Member::currentUser()) {
491
				Session::set("BackURL", null);
492
			}
493
494
			// if no alternate menu items have matched, return a permission error
495
			$messageSet = array(
496
				'default' => _t(
497
					'LeftAndMain.PERMDEFAULT',
498
					"You must be logged in to access the administration area; please enter your credentials below."
499
				),
500
				'alreadyLoggedIn' => _t(
501
					'LeftAndMain.PERMALREADY',
502
					"I'm sorry, but you can't access that part of the CMS.  If you want to log in as someone else, do"
503
					. " so below."
504
				),
505
				'logInAgain' => _t(
506
					'LeftAndMain.PERMAGAIN',
507
					"You have been logged out of the CMS.  If you would like to log in again, enter a username and"
508
					. " password below."
509
				),
510
			);
511
512
			Security::permissionFailure($this, $messageSet);
513
			return;
514
		}
515
516
		// Don't continue if there's already been a redirection request.
517
		if($this->redirectedTo()) {
518
			return;
519
		}
520
521
		// Audit logging hook
522
		if(empty($_REQUEST['executeForm']) && !$this->getRequest()->isAjax()) $this->extend('accessedCMS');
523
524
		// Set the members html editor config
525
		if(Member::currentUser()) {
526
			HTMLEditorConfig::set_active_identifier(Member::currentUser()->getHtmlEditorConfigForCMS());
527
		}
528
529
		// Set default values in the config if missing.  These things can't be defined in the config
530
		// file because insufficient information exists when that is being processed
531
		$htmlEditorConfig = HTMLEditorConfig::get_active();
532
		$htmlEditorConfig->setOption('language', i18n::get_tinymce_lang());
533
534
		Requirements::customScript("
535
			window.ss = window.ss || {};
536
			window.ss.config = " . $this->getCombinedClientConfig() . ";
537
		");
538
539
		Requirements::javascript(FRAMEWORK_ADMIN_DIR . '/client/dist/js/bundle-lib.js');
540
		Requirements::css(FRAMEWORK_ADMIN_DIR . '/client/dist/styles/bundle.css');
541
542
		Requirements::javascript(FRAMEWORK_ADMIN_DIR . '/client/dist/js/bundle-legacy.js');
543
544
		Requirements::add_i18n_javascript(FRAMEWORK_DIR . '/client/lang', false, true);
545
		Requirements::add_i18n_javascript(FRAMEWORK_ADMIN_DIR . '/client/lang', false, true);
546
547
		if ($this->config()->session_keepalive_ping) {
548
			Requirements::javascript(FRAMEWORK_ADMIN_DIR . '/client/dist/js/LeftAndMain.Ping.js');
549
		}
550
551
		if (Director::isDev()) {
552
			// TODO Confuses jQuery.ondemand through document.write()
553
			Requirements::javascript(THIRDPARTY_DIR . '/jquery-entwine/src/jquery.entwine.inspector.js');
554
			Requirements::javascript(FRAMEWORK_ADMIN_DIR . '/client/dist/js/leaktools.js');
555
		}
556
557
		Requirements::javascript(FRAMEWORK_ADMIN_DIR . '/client/dist/js/bundle-framework.js');
558
559
		// Custom requirements
560
		$extraJs = $this->stat('extra_requirements_javascript');
561
562
		if($extraJs) {
563
			foreach($extraJs as $file => $config) {
564
				if(is_numeric($file)) {
565
					$file = $config;
566
				}
567
568
				Requirements::javascript($file);
569
			}
570
		}
571
572
		$extraCss = $this->stat('extra_requirements_css');
573
574 View Code Duplication
		if($extraCss) {
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...
575
			foreach($extraCss as $file => $config) {
576
				if(is_numeric($file)) {
577
					$file = $config;
578
					$config = array();
579
				}
580
581
				Requirements::css($file, isset($config['media']) ? $config['media'] : null);
582
			}
583
		}
584
585
		$extraThemedCss = $this->stat('extra_requirements_themedCss');
586
587 View Code Duplication
		if($extraThemedCss) {
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...
588
			foreach ($extraThemedCss as $file => $config) {
589
				if(is_numeric($file)) {
590
					$file = $config;
591
					$config = array();
592
				}
593
594
				Requirements::themedCSS($file, isset($config['media']) ? $config['media'] : null);
595
			}
596
		}
597
598
		$dummy = null;
599
		$this->extend('init', $dummy);
600
601
		// Assign default cms theme and replace user-specified themes
602
		SSViewer::set_themes($this->config()->admin_themes);
603
604
		//set the reading mode for the admin to stage
605
		Versioned::set_stage(Versioned::DRAFT);
606
	}
607
608
	public function handleRequest(HTTPRequest $request, DataModel $model = null) {
609
		try {
610
			$response = parent::handleRequest($request, $model);
0 ignored issues
show
Bug introduced by
It seems like $model defined by parameter $model on line 608 can be null; however, SilverStripe\Control\Controller::handleRequest() does not accept null, maybe add an additional type check?

It seems like you allow that null is being passed for a parameter, however the function which is called does not seem to accept null.

We recommend to add an additional type check (or disallow null for the parameter):

function notNullable(stdClass $x) { }

// Unsafe
function withoutCheck(stdClass $x = null) {
    notNullable($x);
}

// Safe - Alternative 1: Adding Additional Type-Check
function withCheck(stdClass $x = null) {
    if ($x instanceof stdClass) {
        notNullable($x);
    }
}

// Safe - Alternative 2: Changing Parameter
function withNonNullableParam(stdClass $x) {
    notNullable($x);
}
Loading history...
611
		} catch(ValidationException $e) {
612
			// Nicer presentation of model-level validation errors
613
			$msgs = _t('LeftAndMain.ValidationError', 'Validation error') . ': '
614
				. $e->getMessage();
615
			$e = new HTTPResponse_Exception($msgs, 403);
616
			$errorResponse = $e->getResponse();
617
			$errorResponse->addHeader('Content-Type', 'text/plain');
618
			$errorResponse->addHeader('X-Status', rawurlencode($msgs));
619
			$e->setResponse($errorResponse);
620
			throw $e;
621
		}
622
623
		$title = $this->Title();
624
		if(!$response->getHeader('X-Controller')) $response->addHeader('X-Controller', $this->class);
625
		if(!$response->getHeader('X-Title')) $response->addHeader('X-Title', urlencode($title));
626
627
		// Prevent clickjacking, see https://developer.mozilla.org/en-US/docs/HTTP/X-Frame-Options
628
		$originalResponse = $this->getResponse();
629
		$originalResponse->addHeader('X-Frame-Options', $this->config()->frame_options);
630
		$originalResponse->addHeader('Vary', 'X-Requested-With');
631
632
		return $response;
633
	}
634
635
	/**
636
	 * Overloaded redirection logic to trigger a fake redirect on ajax requests.
637
	 * While this violates HTTP principles, its the only way to work around the
638
	 * fact that browsers handle HTTP redirects opaquely, no intervention via JS is possible.
639
	 * In isolation, that's not a problem - but combined with history.pushState()
640
	 * it means we would request the same redirection URL twice if we want to update the URL as well.
641
	 * See LeftAndMain.js for the required jQuery ajaxComplete handlers.
642
	 *
643
	 * @param string $url
644
	 * @param int $code
645
	 * @return HTTPResponse|string
646
	 */
647
	public function redirect($url, $code=302) {
648
		if($this->getRequest()->isAjax()) {
649
			$response = $this->getResponse();
650
			$response->addHeader('X-ControllerURL', $url);
651
			if($this->getRequest()->getHeader('X-Pjax') && !$response->getHeader('X-Pjax')) {
652
				$response->addHeader('X-Pjax', $this->getRequest()->getHeader('X-Pjax'));
653
			}
654
			$newResponse = new LeftAndMain_HTTPResponse(
655
				$response->getBody(),
656
				$response->getStatusCode(),
657
				$response->getStatusDescription()
658
			);
659
			foreach($response->getHeaders() as $k => $v) {
660
				$newResponse->addHeader($k, $v);
661
			}
662
			$newResponse->setIsFinished(true);
663
			$this->setResponse($newResponse);
664
			return ''; // Actual response will be re-requested by client
0 ignored issues
show
Bug Best Practice introduced by
The return type of return ''; (string) is incompatible with the return type of the parent method SilverStripe\Control\Controller::redirect of type SilverStripe\Control\HTTPResponse|null.

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...
665
		} else {
666
			parent::redirect($url, $code);
667
		}
668
	}
669
670
	/**
671
	 * @param HTTPRequest $request
672
	 * @return HTTPResponse
673
	 */
674
	public function index($request) {
675
		return $this->getResponseNegotiator()->respond($request);
676
	}
677
678
	/**
679
	 * If this is set to true, the "switchView" context in the
680
	 * template is shown, with links to the staging and publish site.
681
	 *
682
	 * @return boolean
683
	 */
684
	public function ShowSwitchView() {
685
		return false;
686
	}
687
688
689
	//------------------------------------------------------------------------------------------//
690
	// Main controllers
691
692
	/**
693
	 * You should implement a Link() function in your subclass of LeftAndMain,
694
	 * to point to the URL of that particular controller.
695
	 *
696
	 * @param string $action
697
	 * @return string
698
	 */
699
	public function Link($action = null) {
700
		// Handle missing url_segments
701
		if($this->config()->url_segment) {
702
			$segment = $this->config()->get('url_segment', Config::FIRST_SET);
703
		} else {
704
			$segment = $this->class;
705
		};
706
707
		$link = Controller::join_links(
708
			AdminRootController::admin_url(),
709
			$segment,
710
			'/', // trailing slash needed if $action is null!
711
			"$action"
712
		);
713
		$this->extend('updateLink', $link);
714
		return $link;
715
	}
716
717
	/**
718
	 * @deprecated 5.0
719
	 */
720
	public static function menu_title_for_class($class) {
721
		Deprecation::notice('5.0', 'Use menu_title() instead');
722
		return static::menu_title($class, false);
723
	}
724
725
	/**
726
	 * Get menu title for this section (translated)
727
	 *
728
	 * @param string $class Optional class name if called on LeftAndMain directly
729
	 * @param bool $localise Determine if menu title should be localised via i18n.
730
	 * @return string Menu title for the given class
731
	 */
732
	public static function menu_title($class = null, $localise = true) {
733
		if($class && is_subclass_of($class, __CLASS__)) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $class 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...
Bug introduced by
Due to PHP Bug #53727, is_subclass_of might return inconsistent results on some PHP versions if __CLASS__ can be an interface. If so, you could instead use ReflectionClass::implementsInterface.
Loading history...
734
			// Respect oveloading of menu_title() in subclasses
735
			return $class::menu_title(null, $localise);
736
		}
737
		if(!$class) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $class of type string|null is loosely compared to false; 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...
738
			$class = get_called_class();
739
		}
740
741
		// Get default class title
742
		$title = Config::inst()->get($class, 'menu_title', Config::FIRST_SET);
743
		if(!$title) {
744
			$title = preg_replace('/Admin$/', '', $class);
745
		}
746
747
		// Check localisation
748
		if(!$localise) {
749
			return $title;
750
		}
751
		return i18n::_t("{$class}.MENUTITLE", $title);
752
	}
753
754
	/**
755
	 * Return styling for the menu icon, if a custom icon is set for this class
756
	 *
757
	 * Example: static $menu-icon = '/path/to/image/';
758
	 * @param string $class
759
	 * @return string
760
	 */
761
	public static function menu_icon_for_class($class) {
762
		$icon = Config::inst()->get($class, 'menu_icon', Config::FIRST_SET);
763
		if (!empty($icon)) {
764
			$class = strtolower(Convert::raw2htmlname(str_replace('\\', '-', $class)));
765
			return ".icon.icon-16.icon-{$class} { background-image: url('{$icon}'); } ";
766
		}
767
		return '';
768
	}
769
770
	/**
771
	 * @param HTTPRequest $request
772
	 * @return HTTPResponse
773
	 * @throws HTTPResponse_Exception
774
     */
775
	public function show($request) {
776
		// TODO Necessary for TableListField URLs to work properly
777
		if($request->param('ID')) $this->setCurrentPageID($request->param('ID'));
778
		return $this->getResponseNegotiator()->respond($request);
779
	}
780
781
	/**
782
	 * Caution: Volatile API.
783
	 *
784
	 * @return PjaxResponseNegotiator
785
	 */
786
	public function getResponseNegotiator() {
787
		if(!$this->responseNegotiator) {
788
			$controller = $this;
789
			$this->responseNegotiator = new PjaxResponseNegotiator(
790
				array(
791
					'CurrentForm' => function() use(&$controller) {
792
						return $controller->getEditForm()->forTemplate();
793
					},
794
					'Content' => function() use(&$controller) {
795
						return $controller->renderWith($controller->getTemplatesWithSuffix('_Content'));
796
					},
797
					'Breadcrumbs' => function() use (&$controller) {
798
						return $controller->renderWith([
799
							'type' => 'Includes',
800
							'SilverStripe\\Admin\\CMSBreadcrumbs'
801
						]);
802
					},
803
					'default' => function() use(&$controller) {
804
						return $controller->renderWith($controller->getViewer('show'));
805
					}
806
				),
807
				$this->getResponse()
808
			);
809
		}
810
		return $this->responseNegotiator;
811
	}
812
813
	//------------------------------------------------------------------------------------------//
814
	// Main UI components
815
816
	/**
817
	 * Returns the main menu of the CMS.  This is also used by init()
818
	 * to work out which sections the user has access to.
819
	 *
820
	 * @param bool $cached
821
	 * @return SS_List
822
	 */
823
	public function MainMenu($cached = true) {
824
		if(!isset($this->_cache_MainMenu) || !$cached) {
825
			// Don't accidentally return a menu if you're not logged in - it's used to determine access.
826
			if(!Member::currentUser()) return new ArrayList();
827
828
			// Encode into DO set
829
			$menu = new ArrayList();
830
			$menuItems = CMSMenu::get_viewable_menu_items();
831
832
			// extra styling for custom menu-icons
833
			$menuIconStyling = '';
834
835
			if($menuItems) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $menuItems of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
836
				/** @var CMSMenuItem $menuItem */
837
				foreach($menuItems as $code => $menuItem) {
838
					// alternate permission checks (in addition to LeftAndMain->canView())
839
					if(
840
						isset($menuItem->controller)
841
						&& $this->hasMethod('alternateMenuDisplayCheck')
842
						&& !$this->alternateMenuDisplayCheck($menuItem->controller)
843
					) {
844
						continue;
845
					}
846
847
					$linkingmode = "link";
848
849
					if($menuItem->controller && get_class($this) == $menuItem->controller) {
850
						$linkingmode = "current";
851
					} else if(strpos($this->Link(), $menuItem->url) !== false) {
852
						if($this->Link() == $menuItem->url) {
853
							$linkingmode = "current";
854
855
						// default menu is the one with a blank {@link url_segment}
856
						} else if(singleton($menuItem->controller)->stat('url_segment') == '') {
857
							if($this->Link() == AdminRootController::admin_url()) {
858
								$linkingmode = "current";
859
							}
860
861
						} else {
862
							$linkingmode = "current";
863
						}
864
					}
865
866
					// already set in CMSMenu::populate_menu(), but from a static pre-controller
867
					// context, so doesn't respect the current user locale in _t() calls - as a workaround,
868
					// we simply call LeftAndMain::menu_title() again
869
					// if we're dealing with a controller
870
					if($menuItem->controller) {
871
						$title = LeftAndMain::menu_title($menuItem->controller);
872
					} else {
873
						$title = $menuItem->title;
874
					}
875
876
					// Provide styling for custom $menu-icon. Done here instead of in
877
					// CMSMenu::populate_menu(), because the icon is part of
878
					// the CMS right pane for the specified class as well...
879
					if($menuItem->controller) {
880
						$menuIcon = LeftAndMain::menu_icon_for_class($menuItem->controller);
881
						if (!empty($menuIcon)) {
882
							$menuIconStyling .= $menuIcon;
883
						}
884
					}
885
886
					$menu->push(new ArrayData(array(
887
						"MenuItem" => $menuItem,
888
						"AttributesHTML" => $menuItem->getAttributesHTML(),
889
						"Title" => Convert::raw2xml($title),
890
						"Code" => $code,
891
						"Icon" => strtolower($code),
892
						"Link" => $menuItem->url,
893
						"LinkingMode" => $linkingmode
894
					)));
895
				}
896
			}
897
			if ($menuIconStyling) Requirements::customCSS($menuIconStyling);
898
899
			$this->_cache_MainMenu = $menu;
0 ignored issues
show
Documentation introduced by
The property _cache_MainMenu does not exist on object<SilverStripe\Admin\LeftAndMain>. 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...
900
		}
901
902
		return $this->_cache_MainMenu;
903
	}
904
905
	public function Menu() {
906
		return $this->renderWith($this->getTemplatesWithSuffix('_Menu'));
907
	}
908
909
	/**
910
	 * @todo Wrap in CMSMenu instance accessor
911
	 * @return ArrayData A single menu entry (see {@link MainMenu})
912
	 */
913
	public function MenuCurrentItem() {
914
		$items = $this->MainMenu();
915
		return $items->find('LinkingMode', 'current');
916
	}
917
918
	/**
919
	 * Return a list of appropriate templates for this class, with the given suffix using
920
	 * {@link SSViewer::get_templates_by_class()}
921
	 *
922
	 * @param string $suffix
923
	 * @return array
924
	 */
925
	public function getTemplatesWithSuffix($suffix) {
926
		$templates = SSViewer::get_templates_by_class(get_class($this), $suffix, __CLASS__);
927
		return SSViewer::chooseTemplate($templates);
928
	}
929
930
	public function Content() {
931
		return $this->renderWith($this->getTemplatesWithSuffix('_Content'));
932
	}
933
934
	/**
935
	 * Render $PreviewPanel content
936
	 *
937
	 * @return DBHTMLText
938
	 */
939
	public function PreviewPanel() {
940
		$template = $this->getTemplatesWithSuffix('_PreviewPanel');
941
		// Only render sections with preview panel
942
		if ($template) {
943
			return $this->renderWith($template);
944
		}
945
	}
946
947
	public function getRecord($id) {
948
		$className = $this->stat('tree_class');
949
		if($className && $id instanceof $className) {
950
			return $id;
951
		} else if($className && $id == 'root') {
952
			return singleton($className);
953
		} else if($className && is_numeric($id)) {
954
			return DataObject::get_by_id($className, $id);
955
		} else {
956
			return false;
957
		}
958
	}
959
960
	/**
961
	 * @param bool $unlinked
962
	 * @return ArrayList
963
	 */
964
	public function Breadcrumbs($unlinked = false) {
965
		$items = new ArrayList(array(
966
			new ArrayData(array(
967
				'Title' => $this->menu_title(),
968
				'Link' => ($unlinked) ? false : $this->Link()
969
			))
970
		));
971
		$record = $this->currentPage();
972
		if($record && $record->exists()) {
973
			if($record->hasExtension('SilverStripe\\ORM\\Hierarchy\\Hierarchy')) {
974
				$ancestors = $record->getAncestors();
975
				$ancestors = new ArrayList(array_reverse($ancestors->toArray()));
976
				$ancestors->push($record);
977
				foreach($ancestors as $ancestor) {
978
					$items->push(new ArrayData(array(
979
						'Title' => ($ancestor->MenuTitle) ? $ancestor->MenuTitle : $ancestor->Title,
980
						'Link' => ($unlinked) ? false : Controller::join_links($this->Link('show'), $ancestor->ID)
981
					)));
982
				}
983
			} else {
984
				$items->push(new ArrayData(array(
985
					'Title' => ($record->MenuTitle) ? $record->MenuTitle : $record->Title,
986
					'Link' => ($unlinked) ? false : Controller::join_links($this->Link('show'), $record->ID)
987
				)));
988
			}
989
		}
990
991
		return $items;
992
	}
993
994
	/**
995
	 * @return String HTML
996
	 */
997
	public function SiteTreeAsUL() {
998
		$html = $this->getSiteTreeFor($this->stat('tree_class'));
999
		$this->extend('updateSiteTreeAsUL', $html);
1000
		return $html;
1001
	}
1002
1003
	/**
1004
	 * Gets the current search filter for this request, if available
1005
	 *
1006
	 * @throws InvalidArgumentException
1007
	 * @return LeftAndMain_SearchFilter
1008
	 */
1009
	protected function getSearchFilter() {
1010
		// Check for given FilterClass
1011
		$params = $this->getRequest()->getVar('q');
1012
		if(empty($params['FilterClass'])) {
1013
			return null;
1014
		}
1015
1016
		// Validate classname
1017
		$filterClass = $params['FilterClass'];
1018
		$filterInfo = new ReflectionClass($filterClass);
1019
		if(!$filterInfo->implementsInterface('SilverStripe\\Admin\\LeftAndMain_SearchFilter')) {
1020
			throw new InvalidArgumentException(sprintf('Invalid filter class passed: %s', $filterClass));
1021
		}
1022
1023
		return Injector::inst()->createWithArgs($filterClass, array($params));
1024
	}
1025
1026
	/**
1027
	 * Get a site tree HTML listing which displays the nodes under the given criteria.
1028
	 *
1029
	 * @param string $className The class of the root object
1030
	 * @param string $rootID The ID of the root object.  If this is null then a complete tree will be
1031
	 *  shown
1032
	 * @param string $childrenMethod The method to call to get the children of the tree. For example,
1033
	 *  Children, AllChildrenIncludingDeleted, or AllHistoricalChildren
1034
	 * @param string $numChildrenMethod
1035
	 * @param callable $filterFunction
1036
	 * @param int $nodeCountThreshold
1037
	 * @return string Nested unordered list with links to each page
1038
	 */
1039
	public function getSiteTreeFor($className, $rootID = null, $childrenMethod = null, $numChildrenMethod = null,
1040
			$filterFunction = null, $nodeCountThreshold = 30) {
1041
1042
		// Filter criteria
1043
		$filter = $this->getSearchFilter();
1044
1045
		// Default childrenMethod and numChildrenMethod
1046
		if(!$childrenMethod) $childrenMethod = ($filter && $filter->getChildrenMethod())
0 ignored issues
show
Bug Best Practice introduced by
The expression $childrenMethod of type string|null is loosely compared to false; 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...
1047
			? $filter->getChildrenMethod()
1048
			: 'AllChildrenIncludingDeleted';
1049
1050
		if(!$numChildrenMethod) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $numChildrenMethod of type string|null is loosely compared to false; 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...
1051
			$numChildrenMethod = 'numChildren';
1052
			if($filter && $filter->getNumChildrenMethod()) {
1053
				$numChildrenMethod = $filter->getNumChildrenMethod();
1054
			}
1055
		}
1056
		if(!$filterFunction && $filter) {
1057
			$filterFunction = function($node) use($filter) {
1058
				return $filter->isPageIncluded($node);
1059
			};
1060
		}
1061
1062
		// Get the tree root
1063
		$record = ($rootID) ? $this->getRecord($rootID) : null;
1064
		$obj = $record ? $record : singleton($className);
1065
1066
		// Get the current page
1067
		// NOTE: This *must* be fetched before markPartialTree() is called, as this
1068
		// causes the Hierarchy::$marked cache to be flushed (@see CMSMain::getRecord)
1069
		// which means that deleted pages stored in the marked tree would be removed
1070
		$currentPage = $this->currentPage();
1071
1072
		// Mark the nodes of the tree to return
1073
		if ($filterFunction) $obj->setMarkingFilterFunction($filterFunction);
1074
1075
		$obj->markPartialTree($nodeCountThreshold, $this, $childrenMethod, $numChildrenMethod);
1076
1077
		// Ensure current page is exposed
1078
		if($currentPage) $obj->markToExpose($currentPage);
1079
1080
		// NOTE: SiteTree/CMSMain coupling :-(
1081
		if(class_exists('SilverStripe\\CMS\\Model\\SiteTree')) {
1082
			SiteTree::prepopulate_permission_cache(
1083
				'CanEditType',
1084
				$obj->markedNodeIDs(),
1085
				'SilverStripe\\CMS\\Model\\SiteTree::can_edit_multiple'
1086
			);
1087
		}
1088
1089
		// getChildrenAsUL is a flexible and complex way of traversing the tree
1090
		$controller = $this;
1091
		$recordController = ($this->stat('tree_class') == 'SilverStripe\\CMS\\Model\\SiteTree')
1092
			?  CMSPageEditController::singleton()
1093
			: $this;
1094
		$titleFn = function(&$child, $numChildrenMethod) use(&$controller, &$recordController, $filter) {
1095
			$link = Controller::join_links($recordController->Link("show"), $child->ID);
1096
			$node = LeftAndMain_TreeNode::create($child, $link, $controller->isCurrentPage($child), $numChildrenMethod, $filter);
1097
			return $node->forTemplate();
1098
		};
1099
1100
		// Limit the amount of nodes shown for performance reasons.
1101
		// Skip the check if we're filtering the tree, since its not clear how many children will
1102
		// match the filter criteria until they're queried (and matched up with previously marked nodes).
1103
		$nodeThresholdLeaf = Config::inst()->get('SilverStripe\\ORM\\Hierarchy\\Hierarchy', 'node_threshold_leaf');
1104
		if($nodeThresholdLeaf && !$filterFunction) {
1105
			$nodeCountCallback = function($parent, $numChildren) use(&$controller, $className, $nodeThresholdLeaf) {
1106
				if ($className !== 'SilverStripe\\CMS\\Model\\SiteTree'
1107
					|| !$parent->ID
1108
					|| $numChildren >= $nodeThresholdLeaf
1109
				) {
1110
					return null;
1111
				}
1112
					return sprintf(
1113
						'<ul><li class="readonly"><span class="item">'
1114
							. '%s (<a href="%s" class="cms-panel-link" data-pjax-target="Content">%s</a>)'
1115
							. '</span></li></ul>',
1116
						_t('LeftAndMain.TooManyPages', 'Too many pages'),
1117
						Controller::join_links(
1118
							$controller->LinkWithSearch($controller->Link()), '
1119
							?view=list&ParentID=' . $parent->ID
1120
						),
1121
						_t(
1122
							'LeftAndMain.ShowAsList',
1123
							'show as list',
1124
							'Show large amount of pages in list instead of tree view'
1125
						)
1126
					);
1127
			};
1128
		} else {
1129
			$nodeCountCallback = null;
1130
		}
1131
1132
		// If the amount of pages exceeds the node thresholds set, use the callback
1133
		$html = null;
1134
		if($obj->ParentID && $nodeCountCallback) {
1135
			$html = $nodeCountCallback($obj, $obj->$numChildrenMethod());
1136
		}
1137
1138
		// Otherwise return the actual tree (which might still filter leaf thresholds on children)
1139
		if(!$html) {
1140
			$html = $obj->getChildrenAsUL(
1141
				"",
1142
				$titleFn,
1143
				CMSPagesController::singleton(),
1144
				true,
1145
				$childrenMethod,
1146
				$numChildrenMethod,
1147
				$nodeCountThreshold,
1148
				$nodeCountCallback
1149
			);
1150
		}
1151
1152
		// Wrap the root if needs be.
1153
		if(!$rootID) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $rootID of type string|null is loosely compared to false; 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...
1154
			$rootLink = $this->Link('show') . '/root';
0 ignored issues
show
Unused Code introduced by
$rootLink 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...
1155
1156
			// This lets us override the tree title with an extension
1157
			if($this->hasMethod('getCMSTreeTitle') && $customTreeTitle = $this->getCMSTreeTitle()) {
1158
				$treeTitle = $customTreeTitle;
1159
			} elseif(class_exists('SilverStripe\\SiteConfig\\SiteConfig')) {
1160
				$siteConfig = SiteConfig::current_site_config();
1161
				$treeTitle =  Convert::raw2xml($siteConfig->Title);
1162
			} else {
1163
				$treeTitle = '...';
1164
			}
1165
1166
			$html = "<ul><li id=\"record-0\" data-id=\"0\" class=\"Root nodelete\"><strong>$treeTitle</strong>"
1167
				. $html . "</li></ul>";
1168
		}
1169
1170
		return $html;
1171
	}
1172
1173
	/**
1174
	 * Get a subtree underneath the request param 'ID'.
1175
	 * If ID = 0, then get the whole tree.
1176
	 *
1177
	 * @param HTTPRequest $request
1178
	 * @return string
1179
	 */
1180
	public function getsubtree($request) {
1181
		$html = $this->getSiteTreeFor(
1182
			$this->stat('tree_class'),
1183
			$request->getVar('ID'),
1184
			null,
1185
			null,
1186
			null,
1187
			$request->getVar('minNodeCount')
1188
		);
1189
1190
		// Trim off the outer tag
1191
		$html = preg_replace('/^[\s\t\r\n]*<ul[^>]*>/','', $html);
1192
		$html = preg_replace('/<\/ul[^>]*>[\s\t\r\n]*$/','', $html);
1193
1194
		return $html;
1195
	}
1196
1197
	/**
1198
	 * Allows requesting a view update on specific tree nodes.
1199
	 * Similar to {@link getsubtree()}, but doesn't enforce loading
1200
	 * all children with the node. Useful to refresh views after
1201
	 * state modifications, e.g. saving a form.
1202
	 *
1203
	 * @param HTTPRequest $request
1204
	 * @return string JSON
1205
	 */
1206
	public function updatetreenodes($request) {
1207
		$data = array();
1208
		$ids = explode(',', $request->getVar('ids'));
1209
		foreach($ids as $id) {
1210
			if($id === "") continue; // $id may be a blank string, which is invalid and should be skipped over
1211
1212
			$record = $this->getRecord($id);
1213
			if(!$record) continue; // In case a page is no longer available
1214
			$recordController = ($this->stat('tree_class') == 'SilverStripe\\CMS\\Model\\SiteTree')
1215
				? CMSPageEditController::singleton()
1216
				: $this;
1217
1218
			// Find the next & previous nodes, for proper positioning (Sort isn't good enough - it's not a raw offset)
1219
			// TODO: These methods should really be in hierarchy - for a start it assumes Sort exists
1220
			$next = $prev = null;
0 ignored issues
show
Unused Code introduced by
$next 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...
1221
1222
			$className = $this->stat('tree_class');
1223
			$next = DataObject::get($className)
1224
				->filter('ParentID', $record->ParentID)
1225
				->filter('Sort:GreaterThan', $record->Sort)
1226
				->first();
1227
1228
			if (!$next) {
1229
				$prev = DataObject::get($className)
1230
					->filter('ParentID', $record->ParentID)
1231
					->filter('Sort:LessThan', $record->Sort)
1232
					->reverse()
1233
					->first();
1234
			}
1235
1236
			$link = Controller::join_links($recordController->Link("show"), $record->ID);
1237
			$html = LeftAndMain_TreeNode::create($record, $link, $this->isCurrentPage($record))
1238
				->forTemplate() . '</li>';
1239
1240
			$data[$id] = array(
1241
				'html' => $html,
1242
				'ParentID' => $record->ParentID,
1243
				'NextID' => $next ? $next->ID : null,
1244
				'PrevID' => $prev ? $prev->ID : null
1245
			);
1246
		}
1247
		$this->getResponse()->addHeader('Content-Type', 'text/json');
1248
		return Convert::raw2json($data);
1249
	}
1250
1251
	/**
1252
	 * Save  handler
1253
	 *
1254
	 * @param array $data
1255
	 * @param Form $form
1256
	 * @return HTTPResponse
1257
	 */
1258
	public function save($data, $form) {
1259
		$request = $this->getRequest();
1260
		$className = $this->stat('tree_class');
1261
1262
		// Existing or new record?
1263
		$id = $data['ID'];
1264
		if(is_numeric($id) && $id > 0) {
1265
			$record = DataObject::get_by_id($className, $id);
1266
			if($record && !$record->canEdit()) {
1267
				return Security::permissionFailure($this);
1268
			}
1269
			if(!$record || !$record->ID) {
1270
				$this->httpError(404, "Bad record ID #" . (int)$id);
1271
			}
1272
		} else {
1273
			if(!singleton($this->stat('tree_class'))->canCreate()) {
1274
				return Security::permissionFailure($this);
1275
			}
1276
			$record = $this->getNewItem($id, false);
1277
		}
1278
1279
		// save form data into record
1280
		$form->saveInto($record, true);
0 ignored issues
show
Documentation introduced by
true is of type boolean, but the function expects a object<SilverStripe\Forms\FieldList>|null.

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...
1281
		$record->write();
1282
		$this->extend('onAfterSave', $record);
1283
		$this->setCurrentPageID($record->ID);
1284
1285
		$message = _t('LeftAndMain.SAVEDUP', 'Saved.');
1286
		if($request->getHeader('X-Formschema-Request')) {
1287
			// Ensure that newly created records have all their data loaded back into the form.
1288
			$form->loadDataFrom($record);
1289
			$form->setMessage($message, 'good');
1290
			$data = $this->getSchemaForForm($form);
1291
			$response = new HTTPResponse(Convert::raw2json($data));
1292
			$response->addHeader('Content-Type', 'application/json');
1293
		} else {
1294
			$response = $this->getResponseNegotiator()->respond($request);
1295
		}
1296
1297
		$response->addHeader('X-Status', rawurlencode($message));
1298
		return $response;
1299
	}
1300
1301
	/**
1302
	 * Create new item.
1303
	 *
1304
	 * @param string|int $id
1305
	 * @param bool $setID
1306
	 * @return DataObject
1307
	 */
1308
	public function getNewItem($id, $setID = true) {
1309
		$class = $this->stat('tree_class');
1310
		$object = Injector::inst()->create($class);
1311
		if($setID) {
1312
			$object->ID = $id;
1313
		}
1314
		return $object;
1315
	}
1316
1317
	public function delete($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...
1318
		$className = $this->stat('tree_class');
1319
1320
		$id = $data['ID'];
1321
		$record = DataObject::get_by_id($className, $id);
1322
		if($record && !$record->canDelete()) return Security::permissionFailure();
1323
		if(!$record || !$record->ID) $this->httpError(404, "Bad record ID #" . (int)$id);
1324
1325
		$record->delete();
1326
1327
		$this->getResponse()->addHeader('X-Status', rawurlencode(_t('LeftAndMain.DELETED', 'Deleted.')));
1328
		return $this->getResponseNegotiator()->respond(
1329
			$this->getRequest(),
1330
			array('currentform' => array($this, 'EmptyForm'))
1331
		);
1332
	}
1333
1334
	/**
1335
	 * Update the position and parent of a tree node.
1336
	 * Only saves the node if changes were made.
1337
	 *
1338
	 * Required data:
1339
	 * - 'ID': The moved node
1340
	 * - 'ParentID': New parent relation of the moved node (0 for root)
1341
	 * - 'SiblingIDs': Array of all sibling nodes to the moved node (incl. the node itself).
1342
	 *   In case of a 'ParentID' change, relates to the new siblings under the new parent.
1343
	 *
1344
	 * @param HTTPRequest $request
1345
	 * @return HTTPResponse JSON string with a
1346
	 * @throws HTTPResponse_Exception
1347
	 */
1348
	public function savetreenode($request) {
1349
		if (!SecurityToken::inst()->checkRequest($request)) {
1350
			return $this->httpError(400);
1351
		}
1352
		if (!Permission::check('SITETREE_REORGANISE') && !Permission::check('ADMIN')) {
1353
			$this->getResponse()->setStatusCode(
1354
				403,
1355
				_t('LeftAndMain.CANT_REORGANISE',
1356
					"You do not have permission to rearange the site tree. Your change was not saved.")
1357
			);
1358
			return;
1359
		}
1360
1361
		$className = $this->stat('tree_class');
1362
		$statusUpdates = array('modified'=>array());
0 ignored issues
show
Unused Code introduced by
$statusUpdates 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...
1363
		$id = $request->requestVar('ID');
1364
		$parentID = $request->requestVar('ParentID');
1365
1366
		if($className == 'SilverStripe\\CMS\\Model\\SiteTree' && $page = DataObject::get_by_id('Page', $id)){
1367
			$root = $page->getParentType();
1368
			if(($parentID == '0' || $root == 'root') && !SiteConfig::current_site_config()->canCreateTopLevel()){
1369
				$this->getResponse()->setStatusCode(
1370
					403,
1371
					_t('LeftAndMain.CANT_REORGANISE',
1372
						"You do not have permission to alter Top level pages. Your change was not saved.")
1373
					);
1374
				return;
1375
			}
1376
		}
1377
1378
		$siblingIDs = $request->requestVar('SiblingIDs');
1379
		$statusUpdates = array('modified'=>array());
1380
		if(!is_numeric($id) || !is_numeric($parentID)) throw new InvalidArgumentException();
1381
1382
		$node = DataObject::get_by_id($className, $id);
1383
		if($node && !$node->canEdit()) return Security::permissionFailure($this);
1384
1385
		if(!$node) {
1386
			$this->getResponse()->setStatusCode(
1387
				500,
1388
				_t('LeftAndMain.PLEASESAVE',
1389
					"Please Save Page: This page could not be updated because it hasn't been saved yet."
1390
				)
1391
			);
1392
			return;
1393
		}
1394
1395
		// Update hierarchy (only if ParentID changed)
1396
		if($node->ParentID != $parentID) {
1397
			$node->ParentID = (int)$parentID;
0 ignored issues
show
Documentation introduced by
The property ParentID does not exist on object<SilverStripe\ORM\DataObject>. 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...
1398
			$node->write();
1399
1400
			$statusUpdates['modified'][$node->ID] = array(
1401
				'TreeTitle'=>$node->TreeTitle
1402
			);
1403
1404
			// Update all dependent pages
1405
			if(class_exists('SilverStripe\\CMS\\Model\\VirtualPage')) {
1406
				$virtualPages = VirtualPage::get()->filter("CopyContentFromID", $node->ID);
1407
				foreach($virtualPages as $virtualPage) {
1408
					$statusUpdates['modified'][$virtualPage->ID] = array(
1409
						'TreeTitle' => $virtualPage->TreeTitle()
1410
					);
1411
				}
1412
			}
1413
1414
			$this->getResponse()->addHeader('X-Status',
1415
				rawurlencode(_t('LeftAndMain.REORGANISATIONSUCCESSFUL', 'Reorganised the site tree successfully.')));
1416
		}
1417
1418
		// Update sorting
1419
		if(is_array($siblingIDs)) {
1420
			$counter = 0;
1421
			foreach($siblingIDs as $id) {
1422
				if($id == $node->ID) {
1423
					$node->Sort = ++$counter;
0 ignored issues
show
Documentation introduced by
The property Sort does not exist on object<SilverStripe\ORM\DataObject>. 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...
1424
					$node->write();
1425
					$statusUpdates['modified'][$node->ID] = array(
1426
						'TreeTitle' => $node->TreeTitle
1427
					);
1428
				} else if(is_numeric($id)) {
1429
					// Nodes that weren't "actually moved" shouldn't be registered as
1430
					// having been edited; do a direct SQL update instead
1431
					++$counter;
1432
					DB::prepared_query(
1433
						"UPDATE \"$className\" SET \"Sort\" = ? WHERE \"ID\" = ?",
1434
						array($counter, $id)
1435
					);
1436
				}
1437
			}
1438
1439
			$this->getResponse()->addHeader('X-Status',
1440
				rawurlencode(_t('LeftAndMain.REORGANISATIONSUCCESSFUL', 'Reorganised the site tree successfully.')));
1441
		}
1442
1443
		return Convert::raw2json($statusUpdates);
0 ignored issues
show
Bug Best Practice introduced by
The return type of return \SilverStripe\Cor...w2json($statusUpdates); (string) is incompatible with the return type documented by SilverStripe\Admin\LeftAndMain::savetreenode of type SilverStripe\Control\HTTPResponse|null.

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...
1444
	}
1445
1446
	public function CanOrganiseSitetree() {
1447
		return !Permission::check('SITETREE_REORGANISE') && !Permission::check('ADMIN') ? false : true;
1448
	}
1449
1450
	/**
1451
	 * Retrieves an edit form, either for display, or to process submitted data.
1452
	 * Also used in the template rendered through {@link Right()} in the $EditForm placeholder.
1453
	 *
1454
	 * This is a "pseudo-abstract" methoed, usually connected to a {@link getEditForm()}
1455
	 * method in an entwine subclass. This method can accept a record identifier,
1456
	 * selected either in custom logic, or through {@link currentPageID()}.
1457
	 * The form usually construct itself from {@link DataObject->getCMSFields()}
1458
	 * for the specific managed subclass defined in {@link LeftAndMain::$tree_class}.
1459
	 *
1460
	 * @param HTTPRequest $request Optionally contains an identifier for the
1461
	 *  record to load into the form.
1462
	 * @return Form Should return a form regardless wether a record has been found.
1463
	 *  Form might be readonly if the current user doesn't have the permission to edit
1464
	 *  the record.
1465
	 */
1466
	/**
1467
	 * @return Form
1468
	 */
1469
	public function EditForm($request = null) {
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...
1470
		return $this->getEditForm();
1471
	}
1472
1473
	/**
1474
	 * Calls {@link SiteTree->getCMSFields()}
1475
	 *
1476
	 * @param Int $id
1477
	 * @param FieldList $fields
1478
	 * @return Form
1479
	 */
1480
	public function getEditForm($id = null, $fields = null) {
1481
		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...
1482
1483
		if(is_object($id)) {
1484
			$record = $id;
1485
		} else {
1486
			$record = $this->getRecord($id);
1487
			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 1487 which is incompatible with the return type documented by SilverStripe\Admin\LeftAndMain::getEditForm of type SilverStripe\Forms\Form|null.
Loading history...
1488
		}
1489
1490
		if($record) {
1491
			$fields = ($fields) ? $fields : $record->getCMSFields();
1492
			if ($fields == null) {
1493
				user_error(
1494
					"getCMSFields() returned null  - it should return a FieldList object.
1495
					Perhaps you forgot to put a return statement at the end of your method?",
1496
					E_USER_ERROR
1497
				);
1498
			}
1499
1500
			// Add hidden fields which are required for saving the record
1501
			// and loading the UI state
1502
			if(!$fields->dataFieldByName('ClassName')) {
1503
				$fields->push(new HiddenField('ClassName'));
1504
			}
1505
1506
			$tree_class = $this->stat('tree_class');
1507
			if(
1508
				$tree_class::has_extension('SilverStripe\\ORM\\Hierarchy\\Hierarchy')
1509
				&& !$fields->dataFieldByName('ParentID')
1510
			) {
1511
				$fields->push(new HiddenField('ParentID'));
1512
			}
1513
1514
			// Added in-line to the form, but plucked into different view by frontend scripts.
1515
			if ($record instanceof CMSPreviewable) {
1516
				/** @skipUpgrade */
1517
				$navField = new LiteralField('SilverStripeNavigator', $this->getSilverStripeNavigator());
1518
				$navField->setAllowHTML(true);
1519
				$fields->push($navField);
1520
			}
1521
1522
			if($record->hasMethod('getAllCMSActions')) {
1523
				$actions = $record->getAllCMSActions();
1524
			} else {
1525
				$actions = $record->getCMSActions();
1526
				// add default actions if none are defined
1527
				if(!$actions || !$actions->count()) {
1528 View Code Duplication
					if($record->hasMethod('canEdit') && $record->canEdit()) {
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...
1529
						$actions->push(
1530
							FormAction::create('save',_t('CMSMain.SAVE','Save'))
1531
								->addExtraClass('ss-ui-action-constructive')->setAttribute('data-icon', 'accept')
1532
						);
1533
					}
1534 View Code Duplication
					if($record->hasMethod('canDelete') && $record->canDelete()) {
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...
1535
						$actions->push(
1536
							FormAction::create('delete',_t('ModelAdmin.DELETE','Delete'))
1537
								->addExtraClass('ss-ui-action-destructive')
1538
						);
1539
					}
1540
				}
1541
			}
1542
1543
			// Use <button> to allow full jQuery UI styling
1544
			$actionsFlattened = $actions->dataFields();
1545
			if($actionsFlattened) {
1546
				/** @var FormAction $action */
1547
				foreach($actionsFlattened as $action) {
1548
					$action->setUseButtonTag(true);
1549
				}
1550
			}
1551
1552
			$negotiator = $this->getResponseNegotiator();
1553
			$form = Form::create(
1554
				$this, "EditForm", $fields, $actions
1555
			)->setHTMLID('Form_EditForm');
1556
			$form->addExtraClass('cms-edit-form');
1557
			$form->loadDataFrom($record);
1558
			$form->setTemplate($this->getTemplatesWithSuffix('_EditForm'));
1559
			$form->setAttribute('data-pjax-fragment', 'CurrentForm');
1560
			$form->setValidationResponseCallback(function() use ($negotiator, $form) {
1561
				$request = $this->getRequest();
1562
				if($request->isAjax() && $negotiator) {
1563
					$form->setupFormErrors();
1564
					$result = $form->forTemplate();
1565
1566
					return $negotiator->respond($request, array(
1567
						'CurrentForm' => function() use($result) {
1568
							return $result;
1569
						}
1570
					));
1571
				}
1572
			});
1573
1574
			// Announce the capability so the frontend can decide whether to allow preview or not.
1575
			if ($record instanceof CMSPreviewable) {
1576
				$form->addExtraClass('cms-previewable');
1577
			}
1578
1579
			// Set this if you want to split up tabs into a separate header row
1580
			// if($form->Fields()->hasTabset()) {
1581
			// 	$form->Fields()->findOrMakeTab('Root')->setTemplate('SilverStripe\\Forms\\CMSTabSet');
1582
			// }
1583
1584
			// Add a default or custom validator.
1585
			// @todo Currently the default Validator.js implementation
1586
			//  adds javascript to the document body, meaning it won't
1587
			//  be included properly if the associated fields are loaded
1588
			//  through ajax. This means only serverside validation
1589
			//  will kick in for pages+validation loaded through ajax.
1590
			//  This will be solved by using less obtrusive javascript validation
1591
			//  in the future, see http://open.silverstripe.com/ticket/2915 and
1592
			//  http://open.silverstripe.com/ticket/3386
1593
			if($record->hasMethod('getCMSValidator')) {
1594
				$validator = $record->getCMSValidator();
1595
				// The clientside (mainly LeftAndMain*.js) rely on ajax responses
1596
				// which can be evaluated as javascript, hence we need
1597
				// to override any global changes to the validation handler.
1598
				if($validator != NULL){
1599
					$form->setValidator($validator);
1600
				}
1601
			} else {
1602
				$form->unsetValidator();
1603
			}
1604
1605
			if($record->hasMethod('canEdit') && !$record->canEdit()) {
1606
				$readonlyFields = $form->Fields()->makeReadonly();
1607
				$form->setFields($readonlyFields);
1608
			}
1609
		} else {
1610
			$form = $this->EmptyForm();
1611
		}
1612
1613
		return $form;
1614
	}
1615
1616
	/**
1617
	 * Returns a placeholder form, used by {@link getEditForm()} if no record is selected.
1618
	 * Our javascript logic always requires a form to be present in the CMS interface.
1619
	 *
1620
	 * @return Form
1621
	 */
1622
	public function EmptyForm() {
1623
		$form = Form::create(
1624
			$this,
1625
			"EditForm",
1626
			new FieldList(
1627
				// new HeaderField(
1628
				// 	'WelcomeHeader',
1629
				// 	$this->getApplicationName()
1630
				// ),
1631
				// new LiteralField(
1632
				// 	'WelcomeText',
1633
				// 	sprintf('<p id="WelcomeMessage">%s %s. %s</p>',
1634
				// 		_t('LeftAndMain_right_ss.WELCOMETO','Welcome to'),
1635
				// 		$this->getApplicationName(),
1636
				// 		_t('CHOOSEPAGE','Please choose an item from the left.')
1637
				// 	)
1638
				// )
1639
			),
1640
			new FieldList()
1641
		)->setHTMLID('Form_EditForm');
1642
		$form->unsetValidator();
1643
		$form->addExtraClass('cms-edit-form');
1644
		$form->addExtraClass('root-form');
1645
		$form->setTemplate($this->getTemplatesWithSuffix('_EditForm'));
1646
		$form->setAttribute('data-pjax-fragment', 'CurrentForm');
1647
1648
		return $form;
1649
	}
1650
1651
	/**
1652
	 * Return the CMS's HTML-editor toolbar
1653
	 */
1654
	public function EditorToolbar() {
1655
		return HTMLEditorField_Toolbar::create($this, "EditorToolbar");
1656
	}
1657
1658
	/**
1659
	 * Renders a panel containing tools which apply to all displayed
1660
	 * "content" (mostly through {@link EditForm()}), for example a tree navigation or a filter panel.
1661
	 * Auto-detects applicable templates by naming convention: "<controller classname>_Tools.ss",
1662
	 * and takes the most specific template (see {@link getTemplatesWithSuffix()}).
1663
	 * To explicitly disable the panel in the subclass, simply create a more specific, empty template.
1664
	 *
1665
	 * @return String HTML
1666
	 */
1667 View Code Duplication
	public function Tools() {
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...
1668
		$templates = $this->getTemplatesWithSuffix('_Tools');
1669
		if($templates) {
1670
			$viewer = new SSViewer($templates);
1671
			return $viewer->process($this);
1672
		} else {
1673
			return false;
1674
		}
1675
	}
1676
1677
	/**
1678
	 * Renders a panel containing tools which apply to the currently displayed edit form.
1679
	 * The main difference to {@link Tools()} is that the panel is displayed within
1680
	 * the element structure of the form panel (rendered through {@link EditForm}).
1681
	 * This means the panel will be loaded alongside new forms, and refreshed upon save,
1682
	 * which can mean a performance hit, depending on how complex your panel logic gets.
1683
	 * Any form fields contained in the returned markup will also be submitted with the main form,
1684
	 * which might be desired depending on the implementation details.
1685
	 *
1686
	 * @return String HTML
1687
	 */
1688 View Code Duplication
	public function EditFormTools() {
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...
1689
		$templates = $this->getTemplatesWithSuffix('_EditFormTools');
1690
		if($templates) {
1691
			$viewer = new SSViewer($templates);
1692
			return $viewer->process($this);
1693
		} else {
1694
			return false;
1695
		}
1696
	}
1697
1698
	/**
1699
	 * Batch Actions Handler
1700
	 */
1701
	public function batchactions() {
1702
		return new CMSBatchActionHandler($this, 'batchactions', $this->stat('tree_class'));
1703
	}
1704
1705
	/**
1706
	 * @return Form
1707
	 */
1708
	public function BatchActionsForm() {
1709
		$actions = $this->batchactions()->batchActionList();
1710
		$actionsMap = array('-1' => _t('LeftAndMain.DropdownBatchActionsDefault', 'Choose an action...')); // Placeholder action
1711
		foreach($actions as $action) {
1712
			$actionsMap[$action->Link] = $action->Title;
1713
		}
1714
1715
		$form = new Form(
1716
			$this,
1717
			'BatchActionsForm',
1718
			new FieldList(
1719
				new HiddenField('csvIDs'),
1720
				DropdownField::create(
1721
					'Action',
1722
					false,
1723
					$actionsMap
1724
				)
1725
					->setAttribute('autocomplete', 'off')
1726
					->setAttribute('data-placeholder', _t('LeftAndMain.DropdownBatchActionsDefault', 'Choose an action...'))
1727
			),
1728
			new FieldList(
1729
				// TODO i18n
1730
				new FormAction('submit', _t('Form.SubmitBtnLabel', "Go"))
1731
			)
1732
		);
1733
		$form->addExtraClass('cms-batch-actions form--no-dividers');
1734
		$form->unsetValidator();
1735
1736
		$this->extend('updateBatchActionsForm', $form);
1737
		return $form;
1738
	}
1739
1740
	public function printable() {
1741
		$form = $this->getEditForm($this->currentPageID());
1742
		if(!$form) return false;
1743
1744
		$form->transform(new PrintableTransformation());
1745
		$form->setActions(null);
0 ignored issues
show
Documentation introduced by
null is of type null, but the function expects a object<SilverStripe\Forms\FieldList>.

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...
1746
1747
		Requirements::clear();
1748
		Requirements::css(FRAMEWORK_ADMIN_DIR . '/dist/css/LeftAndMain_printable.css');
1749
		return array(
1750
			"PrintForm" => $form
1751
		);
1752
	}
1753
1754
	/**
1755
	 * Used for preview controls, mainly links which switch between different states of the page.
1756
	 *
1757
	 * @return DBHTMLText
1758
	 */
1759
	public function getSilverStripeNavigator() {
1760
		$page = $this->currentPage();
1761
		if ($page instanceof CMSPreviewable) {
1762
			$navigator = new SilverStripeNavigator($page);
1763
			return $navigator->renderWith($this->getTemplatesWithSuffix('_SilverStripeNavigator'));
1764
		}
1765
		return null;
1766
	}
1767
1768
	/**
1769
	 * Identifier for the currently shown record,
1770
	 * in most cases a database ID. Inspects the following
1771
	 * sources (in this order):
1772
	 * - GET/POST parameter named 'ID'
1773
	 * - URL parameter named 'ID'
1774
	 * - Session value namespaced by classname, e.g. "CMSMain.currentPage"
1775
	 *
1776
	 * @return int
1777
	 */
1778
	public function currentPageID() {
1779
		if($this->getRequest()->requestVar('ID') && is_numeric($this->getRequest()->requestVar('ID')))	{
1780
			return $this->getRequest()->requestVar('ID');
1781
		} elseif ($this->getRequest()->requestVar('CMSMainCurrentPageID') && is_numeric($this->getRequest()->requestVar('CMSMainCurrentPageID'))) {
1782
			// see GridFieldDetailForm::ItemEditForm
1783
			return $this->getRequest()->requestVar('CMSMainCurrentPageID');
1784
		} elseif (isset($this->urlParams['ID']) && is_numeric($this->urlParams['ID'])) {
1785
			return $this->urlParams['ID'];
1786
		} elseif(Session::get($this->sessionNamespace() . ".currentPage")) {
1787
			return Session::get($this->sessionNamespace() . ".currentPage");
1788
		} else {
1789
			return null;
1790
		}
1791
	}
1792
1793
	/**
1794
	 * Forces the current page to be set in session,
1795
	 * which can be retrieved later through {@link currentPageID()}.
1796
	 * Keep in mind that setting an ID through GET/POST or
1797
	 * as a URL parameter will overrule this value.
1798
	 *
1799
	 * @param int $id
1800
	 */
1801
	public function setCurrentPageID($id) {
1802
		$id = (int)$id;
1803
		Session::set($this->sessionNamespace() . ".currentPage", $id);
1804
	}
1805
1806
	/**
1807
	 * Uses {@link getRecord()} and {@link currentPageID()}
1808
	 * to get the currently selected record.
1809
	 *
1810
	 * @return DataObject
1811
	 */
1812
	public function currentPage() {
1813
		return $this->getRecord($this->currentPageID());
1814
	}
1815
1816
	/**
1817
	 * Compares a given record to the currently selected one (if any).
1818
	 * Used for marking the current tree node.
1819
	 *
1820
	 * @param DataObject $record
1821
	 * @return bool
1822
	 */
1823
	public function isCurrentPage(DataObject $record) {
1824
		return ($record->ID == $this->currentPageID());
1825
	}
1826
1827
	/**
1828
	 * @return String
1829
	 */
1830
	protected function sessionNamespace() {
1831
		$override = $this->stat('session_namespace');
1832
		return $override ? $override : $this->class;
1833
	}
1834
1835
	/**
1836
	 * URL to a previewable record which is shown through this controller.
1837
	 * The controller might not have any previewable content, in which case
1838
	 * this method returns FALSE.
1839
	 *
1840
	 * @return String|boolean
1841
	 */
1842
	public function LinkPreview() {
1843
		return false;
1844
	}
1845
1846
	/**
1847
	 * Return the version number of this application.
1848
	 * Uses the number in <mymodule>/silverstripe_version
1849
	 * (automatically replaced by build scripts).
1850
	 * If silverstripe_version is empty,
1851
	 * then attempts to get it from composer.lock
1852
	 *
1853
	 * @return string
1854
	 */
1855
	public function CMSVersion() {
1856
		$versions = array();
1857
		$modules = array(
1858
			'silverstripe/framework' => array(
1859
				'title' => 'Framework',
1860
				'versionFile' => FRAMEWORK_PATH . '/silverstripe_version',
1861
			)
1862
		);
1863
		if(defined('CMS_PATH')) {
1864
			$modules['silverstripe/cms'] = array(
1865
				'title' => 'CMS',
1866
				'versionFile' => CMS_PATH . '/silverstripe_version',
1867
			);
1868
		}
1869
1870
		// Tries to obtain version number from composer.lock if it exists
1871
		$composerLockPath = BASE_PATH . '/composer.lock';
1872
		if (file_exists($composerLockPath)) {
1873
			$cache = Cache::factory('LeftAndMain_CMSVersion');
1874
			$cacheKey = filemtime($composerLockPath);
1875
			$versions = $cache->load($cacheKey);
1876
			if($versions) {
1877
				$versions = json_decode($versions, true);
1878
			} else {
1879
				$versions = array();
1880
			}
1881
			if(!$versions && $jsonData = file_get_contents($composerLockPath)) {
1882
				$lockData = json_decode($jsonData);
1883
				if($lockData && isset($lockData->packages)) {
1884
					foreach ($lockData->packages as $package) {
1885
						if(
1886
							array_key_exists($package->name, $modules)
1887
							&& isset($package->version)
1888
						) {
1889
							$versions[$package->name] = $package->version;
1890
						}
1891
					}
1892
					$cache->save(json_encode($versions), $cacheKey);
1893
				}
1894
			}
1895
		}
1896
1897
		// Fall back to static version file
1898
		foreach($modules as $moduleName => $moduleSpec) {
1899
			if(!isset($versions[$moduleName])) {
1900
				if($staticVersion = file_get_contents($moduleSpec['versionFile'])) {
1901
					$versions[$moduleName] = $staticVersion;
1902
				} else {
1903
					$versions[$moduleName] = _t('LeftAndMain.VersionUnknown', 'Unknown');
1904
				}
1905
			}
1906
		}
1907
1908
		$out = array();
1909
		foreach($modules as $moduleName => $moduleSpec) {
1910
			$out[] = $modules[$moduleName]['title'] . ': ' . $versions[$moduleName];
1911
		}
1912
		return implode(', ', $out);
1913
	}
1914
1915
	/**
1916
	 * @return array
1917
	 */
1918
	public function SwitchView() {
1919
		if($page = $this->currentPage()) {
1920
			$nav = SilverStripeNavigator::get_for_record($page);
1921
			return $nav['items'];
1922
		}
1923
	}
1924
1925
	/**
1926
	 * @return SiteConfig
1927
	 */
1928
	public function SiteConfig() {
1929
		return (class_exists('SilverStripe\\SiteConfig\\SiteConfig')) ? SiteConfig::current_site_config() : null;
1930
	}
1931
1932
	/**
1933
	 * The href for the anchor on the Silverstripe logo.
1934
	 * Set by calling LeftAndMain::set_application_link()
1935
	 *
1936
	 * @config
1937
	 * @var String
1938
	 */
1939
	private static $application_link = '//www.silverstripe.org/';
1940
1941
	/**
1942
	 * @return String
1943
	 */
1944
	public function ApplicationLink() {
1945
		return $this->stat('application_link');
1946
	}
1947
1948
	/**
1949
	 * The application name. Customisable by calling
1950
	 * LeftAndMain::setApplicationName() - the first parameter.
1951
	 *
1952
	 * @config
1953
	 * @var String
1954
	 */
1955
	private static $application_name = 'SilverStripe';
1956
1957
	/**
1958
	 * Get the application name.
1959
	 *
1960
	 * @return string
1961
	 */
1962
	public function getApplicationName() {
1963
		return $this->stat('application_name');
1964
	}
1965
1966
	/**
1967
	 * @return string
1968
	 */
1969
	public function Title() {
1970
		$app = $this->getApplicationName();
1971
1972
		return ($section = $this->SectionTitle()) ? sprintf('%s - %s', $app, $section) : $app;
1973
	}
1974
1975
	/**
1976
	 * Return the title of the current section. Either this is pulled from
1977
	 * the current panel's menu_title or from the first active menu
1978
	 *
1979
	 * @return string
1980
	 */
1981
	public function SectionTitle() {
1982
		$title = $this->menu_title();
1983
		if($title) {
1984
			return $title;
1985
		}
1986
1987
		foreach($this->MainMenu() as $menuItem) {
1988
			if($menuItem->LinkingMode != 'link') {
1989
				return $menuItem->Title;
1990
			}
1991
		}
1992
	}
1993
1994
	/**
1995
	 * Same as {@link ViewableData->CSSClasses()}, but with a changed name
1996
	 * to avoid problems when using {@link ViewableData->customise()}
1997
	 * (which always returns "ArrayData" from the $original object).
1998
	 *
1999
	 * @return String
2000
	 */
2001
	public function BaseCSSClasses() {
2002
		return $this->CSSClasses('SilverStripe\\Control\\Controller');
2003
	}
2004
2005
	/**
2006
	 * @return String
2007
	 */
2008
	public function Locale() {
2009
		return DBField::create_field('Locale', i18n::get_locale());
2010
	}
2011
2012
	public function providePermissions() {
2013
		$perms = array(
2014
			"CMS_ACCESS_LeftAndMain" => array(
2015
				'name' => _t('CMSMain.ACCESSALLINTERFACES', 'Access to all CMS sections'),
2016
				'category' => _t('Permission.CMS_ACCESS_CATEGORY', 'CMS Access'),
2017
				'help' => _t('CMSMain.ACCESSALLINTERFACESHELP', 'Overrules more specific access settings.'),
2018
				'sort' => -100
2019
			)
2020
		);
2021
2022
		// Add any custom ModelAdmin subclasses. Can't put this on ModelAdmin itself
2023
		// since its marked abstract, and needs to be singleton instanciated.
2024
		foreach(ClassInfo::subclassesFor('SilverStripe\\Admin\\ModelAdmin') as $i => $class) {
2025
			if ($class == 'SilverStripe\\Admin\\ModelAdmin') {
2026
				continue;
2027
			}
2028
			if (ClassInfo::classImplements($class, 'SilverStripe\\Dev\\TestOnly')) {
2029
				continue;
2030
			}
2031
2032
			// Check if modeladmin has explicit required_permission_codes option.
2033
			// If a modeladmin is namespaced you can apply this config to override
2034
			// the default permission generation based on fully qualified class name.
2035
			$code = $this->getRequiredPermissions();
2036
			if (!$code) {
2037
				continue;
2038
			}
2039
			// Get first permission if multiple specified
2040
			if (is_array($code)) {
2041
				$code = reset($code);
2042
			}
2043
			$title = LeftAndMain::menu_title($class);
2044
			$perms[$code] = array(
2045
				'name' => _t(
2046
					'CMSMain.ACCESS',
2047
					"Access to '{title}' section",
2048
					"Item in permission selection identifying the admin section. Example: Access to 'Files & Images'",
2049
					array('title' => $title)
2050
				),
2051
				'category' => _t('Permission.CMS_ACCESS_CATEGORY', 'CMS Access')
2052
			);
2053
		}
2054
2055
		return $perms;
2056
	}
2057
2058
}
2059