Completed
Push — remove-bbcode ( d0705d )
by Sam
09:48
created

LeftAndMain::PreviewPanel()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 4
nc 2
nop 0
dl 0
loc 7
rs 9.4285
c 0
b 0
f 0
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
			'provides' => [
541
				THIRDPARTY_DIR . '/jquery/jquery.js',
542
				THIRDPARTY_DIR . '/jquery-ui/jquery-ui.js',
543
				THIRDPARTY_DIR . '/jquery-entwine/dist/jquery.entwine-dist.js',
544
				THIRDPARTY_DIR . '/jquery-cookie/jquery.cookie.js',
545
				THIRDPARTY_DIR . '/jquery-query/jquery.query.js',
546
				THIRDPARTY_DIR . '/jquery-form/jquery.form.js',
547
				THIRDPARTY_DIR . '/jquery-ondemand/jquery.ondemand.js',
548
				THIRDPARTY_DIR . '/jquery-changetracker/lib/jquery.changetracker.js',
549
				THIRDPARTY_DIR . '/jstree/jquery.jstree.js',
550
				FRAMEWORK_ADMIN_DIR . '/thirdparty/jquery-notice/jquery.notice.js',
551
				FRAMEWORK_ADMIN_DIR . '/thirdparty/jsizes/lib/jquery.sizes.js',
552
				FRAMEWORK_ADMIN_DIR . '/thirdparty/jlayout/lib/jlayout.border.js',
553
				FRAMEWORK_ADMIN_DIR . '/thirdparty/jlayout/lib/jquery.jlayout.js',
554
				FRAMEWORK_ADMIN_DIR . '/thirdparty/chosen/chosen/chosen.jquery.js',
555
				FRAMEWORK_ADMIN_DIR . '/thirdparty/jquery-hoverIntent/jquery.hoverIntent.js',
556
				FRAMEWORK_DIR . '/client/dist/js/TreeDropdownField.js',
557
				FRAMEWORK_DIR . '/client/dist/js/DateField.js',
558
				FRAMEWORK_DIR . '/client/dist/js/HtmlEditorField.js',
559
				FRAMEWORK_DIR . '/client/dist/js/TabSet.js',
560
				FRAMEWORK_DIR . '/client/dist/js/GridField.js',
561
				FRAMEWORK_DIR . '/client/dist/js/i18n.js',
562
				FRAMEWORK_ADMIN_DIR . '/client/dist/js/sspath.js',
563
				FRAMEWORK_ADMIN_DIR . '/client/dist/js/ssui.core.js'
564
			]
565
		]);
566
567
		Requirements::javascript(FRAMEWORK_ADMIN_DIR . '/client/dist/js/bundle-legacy.js', [
568
			'provides' => [
569
				FRAMEWORK_ADMIN_DIR . '/client/dist/js/LeftAndMain.Layout.js',
570
				FRAMEWORK_ADMIN_DIR . '/client/dist/js/LeftAndMain.js',
571
				FRAMEWORK_ADMIN_DIR . '/client/dist/js/LeftAndMain.ActionTabSet.js',
572
				FRAMEWORK_ADMIN_DIR . '/client/dist/js/LeftAndMain.Panel.js',
573
				FRAMEWORK_ADMIN_DIR . '/client/dist/js/LeftAndMain.Tree.js',
574
				FRAMEWORK_ADMIN_DIR . '/client/dist/js/LeftAndMain.Content.js',
575
				FRAMEWORK_ADMIN_DIR . '/client/dist/js/LeftAndMain.EditForm.js',
576
				FRAMEWORK_ADMIN_DIR . '/client/dist/js/LeftAndMain.Menu.js',
577
				FRAMEWORK_ADMIN_DIR . '/client/dist/js/LeftAndMain.Preview.js',
578
				FRAMEWORK_ADMIN_DIR . '/client/dist/js/LeftAndMain.BatchActions.js',
579
				FRAMEWORK_ADMIN_DIR . '/client/dist/js/LeftAndMain.FieldHelp.js',
580
				FRAMEWORK_ADMIN_DIR . '/client/dist/js/LeftAndMain.FieldDescriptionToggle.js',
581
				FRAMEWORK_ADMIN_DIR . '/client/dist/js/LeftAndMain.TreeDropdownField.js',
582
				FRAMEWORK_ADMIN_DIR . '/client/dist/js/AddToCampaignForm.js'
583
			]
584
		]);
585
586
		Requirements::add_i18n_javascript(FRAMEWORK_DIR . '/client/lang', false, true);
587
		Requirements::add_i18n_javascript(FRAMEWORK_ADMIN_DIR . '/client/lang', false, true);
588
589
		if ($this->config()->session_keepalive_ping) {
590
			Requirements::javascript(FRAMEWORK_ADMIN_DIR . '/client/dist/js/LeftAndMain.Ping.js');
591
		}
592
593
		if (Director::isDev()) {
594
			// TODO Confuses jQuery.ondemand through document.write()
595
			Requirements::javascript(THIRDPARTY_DIR . '/jquery-entwine/src/jquery.entwine.inspector.js');
596
			Requirements::javascript(FRAMEWORK_ADMIN_DIR . '/client/dist/js/leaktools.js');
597
		}
598
599
		Requirements::javascript(FRAMEWORK_ADMIN_DIR . '/client/dist/js/bundle-framework.js');
600
601
		Requirements::css(FRAMEWORK_ADMIN_DIR . '/thirdparty/jquery-notice/jquery.notice.css');
602
		Requirements::css(THIRDPARTY_DIR . '/jquery-ui-themes/smoothness/jquery-ui.css');
603
		Requirements::css(THIRDPARTY_DIR . '/jstree/themes/apple/style.css');
604
		Requirements::css(FRAMEWORK_DIR . '/client/dist/styles/TreeDropdownField.css');
605
		Requirements::css(FRAMEWORK_ADMIN_DIR . '/client/dist/styles/bundle.css');
606
		Requirements::css(FRAMEWORK_DIR . '/client/dist/styles/GridField.css');
607
608
		// Custom requirements
609
		$extraJs = $this->stat('extra_requirements_javascript');
610
611
		if($extraJs) {
612
			foreach($extraJs as $file => $config) {
613
				if(is_numeric($file)) {
614
					$file = $config;
615
				}
616
617
				Requirements::javascript($file);
618
			}
619
		}
620
621
		$extraCss = $this->stat('extra_requirements_css');
622
623 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...
624
			foreach($extraCss as $file => $config) {
625
				if(is_numeric($file)) {
626
					$file = $config;
627
					$config = array();
628
				}
629
630
				Requirements::css($file, isset($config['media']) ? $config['media'] : null);
631
			}
632
		}
633
634
		$extraThemedCss = $this->stat('extra_requirements_themedCss');
635
636 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...
637
			foreach ($extraThemedCss as $file => $config) {
638
				if(is_numeric($file)) {
639
					$file = $config;
640
					$config = array();
641
				}
642
643
				Requirements::themedCSS($file, isset($config['media']) ? $config['media'] : null);
644
			}
645
		}
646
647
		$dummy = null;
648
		$this->extend('init', $dummy);
649
650
		// Assign default cms theme and replace user-specified themes
651
		SSViewer::set_themes($this->config()->admin_themes);
652
653
		//set the reading mode for the admin to stage
654
		Versioned::set_stage(Versioned::DRAFT);
655
	}
656
657
	public function handleRequest(HTTPRequest $request, DataModel $model = null) {
658
		try {
659
			$response = parent::handleRequest($request, $model);
0 ignored issues
show
Bug introduced by
It seems like $model defined by parameter $model on line 657 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...
660
		} catch(ValidationException $e) {
661
			// Nicer presentation of model-level validation errors
662
			$msgs = _t('LeftAndMain.ValidationError', 'Validation error') . ': '
663
				. $e->getMessage();
664
			$e = new HTTPResponse_Exception($msgs, 403);
665
			$errorResponse = $e->getResponse();
666
			$errorResponse->addHeader('Content-Type', 'text/plain');
667
			$errorResponse->addHeader('X-Status', rawurlencode($msgs));
668
			$e->setResponse($errorResponse);
669
			throw $e;
670
		}
671
672
		$title = $this->Title();
673
		if(!$response->getHeader('X-Controller')) $response->addHeader('X-Controller', $this->class);
674
		if(!$response->getHeader('X-Title')) $response->addHeader('X-Title', urlencode($title));
675
676
		// Prevent clickjacking, see https://developer.mozilla.org/en-US/docs/HTTP/X-Frame-Options
677
		$originalResponse = $this->getResponse();
678
		$originalResponse->addHeader('X-Frame-Options', $this->config()->frame_options);
679
		$originalResponse->addHeader('Vary', 'X-Requested-With');
680
681
		return $response;
682
	}
683
684
	/**
685
	 * Overloaded redirection logic to trigger a fake redirect on ajax requests.
686
	 * While this violates HTTP principles, its the only way to work around the
687
	 * fact that browsers handle HTTP redirects opaquely, no intervention via JS is possible.
688
	 * In isolation, that's not a problem - but combined with history.pushState()
689
	 * it means we would request the same redirection URL twice if we want to update the URL as well.
690
	 * See LeftAndMain.js for the required jQuery ajaxComplete handlers.
691
	 *
692
	 * @param string $url
693
	 * @param int $code
694
	 * @return HTTPResponse|string
695
	 */
696
	public function redirect($url, $code=302) {
697
		if($this->getRequest()->isAjax()) {
698
			$response = $this->getResponse();
699
			$response->addHeader('X-ControllerURL', $url);
700
			if($this->getRequest()->getHeader('X-Pjax') && !$response->getHeader('X-Pjax')) {
701
				$response->addHeader('X-Pjax', $this->getRequest()->getHeader('X-Pjax'));
702
			}
703
			$newResponse = new LeftAndMain_HTTPResponse(
704
				$response->getBody(),
705
				$response->getStatusCode(),
706
				$response->getStatusDescription()
707
			);
708
			foreach($response->getHeaders() as $k => $v) {
709
				$newResponse->addHeader($k, $v);
710
			}
711
			$newResponse->setIsFinished(true);
712
			$this->setResponse($newResponse);
713
			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...
714
		} else {
715
			parent::redirect($url, $code);
716
		}
717
	}
718
719
	/**
720
	 * @param HTTPRequest $request
721
	 * @return HTTPResponse
722
	 */
723
	public function index($request) {
724
		return $this->getResponseNegotiator()->respond($request);
725
	}
726
727
	/**
728
	 * If this is set to true, the "switchView" context in the
729
	 * template is shown, with links to the staging and publish site.
730
	 *
731
	 * @return boolean
732
	 */
733
	public function ShowSwitchView() {
734
		return false;
735
	}
736
737
738
	//------------------------------------------------------------------------------------------//
739
	// Main controllers
740
741
	/**
742
	 * You should implement a Link() function in your subclass of LeftAndMain,
743
	 * to point to the URL of that particular controller.
744
	 *
745
	 * @param string $action
746
	 * @return string
747
	 */
748
	public function Link($action = null) {
749
		// Handle missing url_segments
750
		if($this->config()->url_segment) {
751
			$segment = $this->config()->get('url_segment', Config::FIRST_SET);
752
		} else {
753
			$segment = $this->class;
754
		};
755
756
		$link = Controller::join_links(
757
			AdminRootController::admin_url(),
758
			$segment,
759
			'/', // trailing slash needed if $action is null!
760
			"$action"
761
		);
762
		$this->extend('updateLink', $link);
763
		return $link;
764
	}
765
766
	/**
767
	 * @deprecated 5.0
768
	 */
769
	public static function menu_title_for_class($class) {
770
		Deprecation::notice('5.0', 'Use menu_title() instead');
771
		return static::menu_title($class, false);
772
	}
773
774
	/**
775
	 * Get menu title for this section (translated)
776
	 *
777
	 * @param string $class Optional class name if called on LeftAndMain directly
778
	 * @param bool $localise Determine if menu title should be localised via i18n.
779
	 * @return string Menu title for the given class
780
	 */
781
	public static function menu_title($class = null, $localise = true) {
782
		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...
783
			// Respect oveloading of menu_title() in subclasses
784
			return $class::menu_title(null, $localise);
785
		}
786
		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...
787
			$class = get_called_class();
788
		}
789
790
		// Get default class title
791
		$title = Config::inst()->get($class, 'menu_title', Config::FIRST_SET);
792
		if(!$title) {
793
			$title = preg_replace('/Admin$/', '', $class);
794
		}
795
796
		// Check localisation
797
		if(!$localise) {
798
			return $title;
799
		}
800
		return i18n::_t("{$class}.MENUTITLE", $title);
801
	}
802
803
	/**
804
	 * Return styling for the menu icon, if a custom icon is set for this class
805
	 *
806
	 * Example: static $menu-icon = '/path/to/image/';
807
	 * @param string $class
808
	 * @return string
809
	 */
810
	public static function menu_icon_for_class($class) {
811
		$icon = Config::inst()->get($class, 'menu_icon', Config::FIRST_SET);
812
		if (!empty($icon)) {
813
			$class = strtolower(Convert::raw2htmlname(str_replace('\\', '-', $class)));
814
			return ".icon.icon-16.icon-{$class} { background-image: url('{$icon}'); } ";
815
		}
816
		return '';
817
	}
818
819
	/**
820
	 * @param HTTPRequest $request
821
	 * @return HTTPResponse
822
	 * @throws HTTPResponse_Exception
823
     */
824
	public function show($request) {
825
		// TODO Necessary for TableListField URLs to work properly
826
		if($request->param('ID')) $this->setCurrentPageID($request->param('ID'));
827
		return $this->getResponseNegotiator()->respond($request);
828
	}
829
830
	/**
831
	 * Caution: Volatile API.
832
	 *
833
	 * @return PjaxResponseNegotiator
834
	 */
835
	public function getResponseNegotiator() {
836
		if(!$this->responseNegotiator) {
837
			$controller = $this;
838
			$this->responseNegotiator = new PjaxResponseNegotiator(
839
				array(
840
					'CurrentForm' => function() use(&$controller) {
841
						return $controller->getEditForm()->forTemplate();
842
					},
843
					'Content' => function() use(&$controller) {
844
						return $controller->renderWith($controller->getTemplatesWithSuffix('_Content'));
845
					},
846
					'Breadcrumbs' => function() use (&$controller) {
847
						return $controller->renderWith([
848
							'type' => 'Includes',
849
							'SilverStripe\\Admin\\CMSBreadcrumbs'
850
						]);
851
					},
852
					'default' => function() use(&$controller) {
853
						return $controller->renderWith($controller->getViewer('show'));
854
					}
855
				),
856
				$this->getResponse()
857
			);
858
		}
859
		return $this->responseNegotiator;
860
	}
861
862
	//------------------------------------------------------------------------------------------//
863
	// Main UI components
864
865
	/**
866
	 * Returns the main menu of the CMS.  This is also used by init()
867
	 * to work out which sections the user has access to.
868
	 *
869
	 * @param bool $cached
870
	 * @return SS_List
871
	 */
872
	public function MainMenu($cached = true) {
873
		if(!isset($this->_cache_MainMenu) || !$cached) {
874
			// Don't accidentally return a menu if you're not logged in - it's used to determine access.
875
			if(!Member::currentUser()) return new ArrayList();
876
877
			// Encode into DO set
878
			$menu = new ArrayList();
879
			$menuItems = CMSMenu::get_viewable_menu_items();
880
881
			// extra styling for custom menu-icons
882
			$menuIconStyling = '';
883
884
			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...
885
				/** @var CMSMenuItem $menuItem */
886
				foreach($menuItems as $code => $menuItem) {
887
					// alternate permission checks (in addition to LeftAndMain->canView())
888
					if(
889
						isset($menuItem->controller)
890
						&& $this->hasMethod('alternateMenuDisplayCheck')
891
						&& !$this->alternateMenuDisplayCheck($menuItem->controller)
892
					) {
893
						continue;
894
					}
895
896
					$linkingmode = "link";
897
898
					if($menuItem->controller && get_class($this) == $menuItem->controller) {
899
						$linkingmode = "current";
900
					} else if(strpos($this->Link(), $menuItem->url) !== false) {
901
						if($this->Link() == $menuItem->url) {
902
							$linkingmode = "current";
903
904
						// default menu is the one with a blank {@link url_segment}
905
						} else if(singleton($menuItem->controller)->stat('url_segment') == '') {
906
							if($this->Link() == AdminRootController::admin_url()) {
907
								$linkingmode = "current";
908
							}
909
910
						} else {
911
							$linkingmode = "current";
912
						}
913
					}
914
915
					// already set in CMSMenu::populate_menu(), but from a static pre-controller
916
					// context, so doesn't respect the current user locale in _t() calls - as a workaround,
917
					// we simply call LeftAndMain::menu_title() again
918
					// if we're dealing with a controller
919
					if($menuItem->controller) {
920
						$title = LeftAndMain::menu_title($menuItem->controller);
921
					} else {
922
						$title = $menuItem->title;
923
					}
924
925
					// Provide styling for custom $menu-icon. Done here instead of in
926
					// CMSMenu::populate_menu(), because the icon is part of
927
					// the CMS right pane for the specified class as well...
928
					if($menuItem->controller) {
929
						$menuIcon = LeftAndMain::menu_icon_for_class($menuItem->controller);
930
						if (!empty($menuIcon)) {
931
							$menuIconStyling .= $menuIcon;
932
						}
933
					}
934
935
					$menu->push(new ArrayData(array(
936
						"MenuItem" => $menuItem,
937
						"AttributesHTML" => $menuItem->getAttributesHTML(),
938
						"Title" => Convert::raw2xml($title),
939
						"Code" => $code,
940
						"Icon" => strtolower($code),
941
						"Link" => $menuItem->url,
942
						"LinkingMode" => $linkingmode
943
					)));
944
				}
945
			}
946
			if ($menuIconStyling) Requirements::customCSS($menuIconStyling);
947
948
			$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...
949
		}
950
951
		return $this->_cache_MainMenu;
952
	}
953
954
	public function Menu() {
955
		return $this->renderWith($this->getTemplatesWithSuffix('_Menu'));
956
	}
957
958
	/**
959
	 * @todo Wrap in CMSMenu instance accessor
960
	 * @return ArrayData A single menu entry (see {@link MainMenu})
961
	 */
962
	public function MenuCurrentItem() {
963
		$items = $this->MainMenu();
964
		return $items->find('LinkingMode', 'current');
965
	}
966
967
	/**
968
	 * Return a list of appropriate templates for this class, with the given suffix using
969
	 * {@link SSViewer::get_templates_by_class()}
970
	 *
971
	 * @param string $suffix
972
	 * @return array
973
	 */
974
	public function getTemplatesWithSuffix($suffix) {
975
		$templates = SSViewer::get_templates_by_class(get_class($this), $suffix, __CLASS__);
976
		return SSViewer::chooseTemplate($templates);
977
	}
978
979
	public function Content() {
980
		return $this->renderWith($this->getTemplatesWithSuffix('_Content'));
981
	}
982
983
	/**
984
	 * Render $PreviewPanel content
985
	 *
986
	 * @return DBHTMLText
987
	 */
988
	public function PreviewPanel() {
989
		$template = $this->getTemplatesWithSuffix('_PreviewPanel');
990
		// Only render sections with preview panel
991
		if ($template) {
992
			return $this->renderWith($template);
993
		}
994
	}
995
996
	public function getRecord($id) {
997
		$className = $this->stat('tree_class');
998
		if($className && $id instanceof $className) {
999
			return $id;
1000
		} else if($className && $id == 'root') {
1001
			return singleton($className);
1002
		} else if($className && is_numeric($id)) {
1003
			return DataObject::get_by_id($className, $id);
1004
		} else {
1005
			return false;
1006
		}
1007
	}
1008
1009
	/**
1010
	 * @param bool $unlinked
1011
	 * @return ArrayList
1012
	 */
1013
	public function Breadcrumbs($unlinked = false) {
1014
		$items = new ArrayList(array(
1015
			new ArrayData(array(
1016
				'Title' => $this->menu_title(),
1017
				'Link' => ($unlinked) ? false : $this->Link()
1018
			))
1019
		));
1020
		$record = $this->currentPage();
1021
		if($record && $record->exists()) {
1022
			if($record->hasExtension('SilverStripe\\ORM\\Hierarchy\\Hierarchy')) {
1023
				$ancestors = $record->getAncestors();
1024
				$ancestors = new ArrayList(array_reverse($ancestors->toArray()));
1025
				$ancestors->push($record);
1026
				foreach($ancestors as $ancestor) {
1027
					$items->push(new ArrayData(array(
1028
						'Title' => ($ancestor->MenuTitle) ? $ancestor->MenuTitle : $ancestor->Title,
1029
						'Link' => ($unlinked) ? false : Controller::join_links($this->Link('show'), $ancestor->ID)
1030
					)));
1031
				}
1032
			} else {
1033
				$items->push(new ArrayData(array(
1034
					'Title' => ($record->MenuTitle) ? $record->MenuTitle : $record->Title,
1035
					'Link' => ($unlinked) ? false : Controller::join_links($this->Link('show'), $record->ID)
1036
				)));
1037
			}
1038
		}
1039
1040
		return $items;
1041
	}
1042
1043
	/**
1044
	 * @return String HTML
1045
	 */
1046
	public function SiteTreeAsUL() {
1047
		$html = $this->getSiteTreeFor($this->stat('tree_class'));
1048
		$this->extend('updateSiteTreeAsUL', $html);
1049
		return $html;
1050
	}
1051
1052
	/**
1053
	 * Gets the current search filter for this request, if available
1054
	 *
1055
	 * @throws InvalidArgumentException
1056
	 * @return LeftAndMain_SearchFilter
1057
	 */
1058
	protected function getSearchFilter() {
1059
		// Check for given FilterClass
1060
		$params = $this->getRequest()->getVar('q');
1061
		if(empty($params['FilterClass'])) {
1062
			return null;
1063
		}
1064
1065
		// Validate classname
1066
		$filterClass = $params['FilterClass'];
1067
		$filterInfo = new ReflectionClass($filterClass);
1068
		if(!$filterInfo->implementsInterface('SilverStripe\\Admin\\LeftAndMain_SearchFilter')) {
1069
			throw new InvalidArgumentException(sprintf('Invalid filter class passed: %s', $filterClass));
1070
		}
1071
1072
		return Injector::inst()->createWithArgs($filterClass, array($params));
1073
	}
1074
1075
	/**
1076
	 * Get a site tree HTML listing which displays the nodes under the given criteria.
1077
	 *
1078
	 * @param string $className The class of the root object
1079
	 * @param string $rootID The ID of the root object.  If this is null then a complete tree will be
1080
	 *  shown
1081
	 * @param string $childrenMethod The method to call to get the children of the tree. For example,
1082
	 *  Children, AllChildrenIncludingDeleted, or AllHistoricalChildren
1083
	 * @param string $numChildrenMethod
1084
	 * @param callable $filterFunction
1085
	 * @param int $nodeCountThreshold
1086
	 * @return string Nested unordered list with links to each page
1087
	 */
1088
	public function getSiteTreeFor($className, $rootID = null, $childrenMethod = null, $numChildrenMethod = null,
1089
			$filterFunction = null, $nodeCountThreshold = 30) {
1090
1091
		// Filter criteria
1092
		$filter = $this->getSearchFilter();
1093
1094
		// Default childrenMethod and numChildrenMethod
1095
		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...
1096
			? $filter->getChildrenMethod()
1097
			: 'AllChildrenIncludingDeleted';
1098
1099
		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...
1100
			$numChildrenMethod = 'numChildren';
1101
			if($filter && $filter->getNumChildrenMethod()) {
1102
				$numChildrenMethod = $filter->getNumChildrenMethod();
1103
			}
1104
		}
1105
		if(!$filterFunction && $filter) {
1106
			$filterFunction = function($node) use($filter) {
1107
				return $filter->isPageIncluded($node);
1108
			};
1109
		}
1110
1111
		// Get the tree root
1112
		$record = ($rootID) ? $this->getRecord($rootID) : null;
1113
		$obj = $record ? $record : singleton($className);
1114
1115
		// Get the current page
1116
		// NOTE: This *must* be fetched before markPartialTree() is called, as this
1117
		// causes the Hierarchy::$marked cache to be flushed (@see CMSMain::getRecord)
1118
		// which means that deleted pages stored in the marked tree would be removed
1119
		$currentPage = $this->currentPage();
1120
1121
		// Mark the nodes of the tree to return
1122
		if ($filterFunction) $obj->setMarkingFilterFunction($filterFunction);
1123
1124
		$obj->markPartialTree($nodeCountThreshold, $this, $childrenMethod, $numChildrenMethod);
1125
1126
		// Ensure current page is exposed
1127
		if($currentPage) $obj->markToExpose($currentPage);
1128
1129
		// NOTE: SiteTree/CMSMain coupling :-(
1130
		if(class_exists('SilverStripe\\CMS\\Model\\SiteTree')) {
1131
			SiteTree::prepopulate_permission_cache(
1132
				'CanEditType',
1133
				$obj->markedNodeIDs(),
1134
				'SilverStripe\\CMS\\Model\\SiteTree::can_edit_multiple'
1135
			);
1136
		}
1137
1138
		// getChildrenAsUL is a flexible and complex way of traversing the tree
1139
		$controller = $this;
1140
		$recordController = ($this->stat('tree_class') == 'SilverStripe\\CMS\\Model\\SiteTree')
1141
			?  CMSPageEditController::singleton()
1142
			: $this;
1143
		$titleFn = function(&$child, $numChildrenMethod) use(&$controller, &$recordController, $filter) {
1144
			$link = Controller::join_links($recordController->Link("show"), $child->ID);
1145
			$node = LeftAndMain_TreeNode::create($child, $link, $controller->isCurrentPage($child), $numChildrenMethod, $filter);
1146
			return $node->forTemplate();
1147
		};
1148
1149
		// Limit the amount of nodes shown for performance reasons.
1150
		// Skip the check if we're filtering the tree, since its not clear how many children will
1151
		// match the filter criteria until they're queried (and matched up with previously marked nodes).
1152
		$nodeThresholdLeaf = Config::inst()->get('SilverStripe\\ORM\\Hierarchy\\Hierarchy', 'node_threshold_leaf');
1153
		if($nodeThresholdLeaf && !$filterFunction) {
1154
			$nodeCountCallback = function($parent, $numChildren) use(&$controller, $className, $nodeThresholdLeaf) {
1155
				if ($className !== 'SilverStripe\\CMS\\Model\\SiteTree'
1156
					|| !$parent->ID
1157
					|| $numChildren >= $nodeThresholdLeaf
1158
				) {
1159
					return null;
1160
				}
1161
					return sprintf(
1162
						'<ul><li class="readonly"><span class="item">'
1163
							. '%s (<a href="%s" class="cms-panel-link" data-pjax-target="Content">%s</a>)'
1164
							. '</span></li></ul>',
1165
						_t('LeftAndMain.TooManyPages', 'Too many pages'),
1166
						Controller::join_links(
1167
							$controller->LinkWithSearch($controller->Link()), '
1168
							?view=list&ParentID=' . $parent->ID
1169
						),
1170
						_t(
1171
							'LeftAndMain.ShowAsList',
1172
							'show as list',
1173
							'Show large amount of pages in list instead of tree view'
1174
						)
1175
					);
1176
			};
1177
		} else {
1178
			$nodeCountCallback = null;
1179
		}
1180
1181
		// If the amount of pages exceeds the node thresholds set, use the callback
1182
		$html = null;
1183
		if($obj->ParentID && $nodeCountCallback) {
1184
			$html = $nodeCountCallback($obj, $obj->$numChildrenMethod());
1185
		}
1186
1187
		// Otherwise return the actual tree (which might still filter leaf thresholds on children)
1188
		if(!$html) {
1189
			$html = $obj->getChildrenAsUL(
1190
				"",
1191
				$titleFn,
1192
				CMSPagesController::singleton(),
1193
				true,
1194
				$childrenMethod,
1195
				$numChildrenMethod,
1196
				$nodeCountThreshold,
1197
				$nodeCountCallback
1198
			);
1199
		}
1200
1201
		// Wrap the root if needs be.
1202
		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...
1203
			$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...
1204
1205
			// This lets us override the tree title with an extension
1206
			if($this->hasMethod('getCMSTreeTitle') && $customTreeTitle = $this->getCMSTreeTitle()) {
1207
				$treeTitle = $customTreeTitle;
1208
			} elseif(class_exists('SilverStripe\\SiteConfig\\SiteConfig')) {
1209
				$siteConfig = SiteConfig::current_site_config();
1210
				$treeTitle =  Convert::raw2xml($siteConfig->Title);
1211
			} else {
1212
				$treeTitle = '...';
1213
			}
1214
1215
			$html = "<ul><li id=\"record-0\" data-id=\"0\" class=\"Root nodelete\"><strong>$treeTitle</strong>"
1216
				. $html . "</li></ul>";
1217
		}
1218
1219
		return $html;
1220
	}
1221
1222
	/**
1223
	 * Get a subtree underneath the request param 'ID'.
1224
	 * If ID = 0, then get the whole tree.
1225
	 *
1226
	 * @param HTTPRequest $request
1227
	 * @return string
1228
	 */
1229
	public function getsubtree($request) {
1230
		$html = $this->getSiteTreeFor(
1231
			$this->stat('tree_class'),
1232
			$request->getVar('ID'),
1233
			null,
1234
			null,
1235
			null,
1236
			$request->getVar('minNodeCount')
1237
		);
1238
1239
		// Trim off the outer tag
1240
		$html = preg_replace('/^[\s\t\r\n]*<ul[^>]*>/','', $html);
1241
		$html = preg_replace('/<\/ul[^>]*>[\s\t\r\n]*$/','', $html);
1242
1243
		return $html;
1244
	}
1245
1246
	/**
1247
	 * Allows requesting a view update on specific tree nodes.
1248
	 * Similar to {@link getsubtree()}, but doesn't enforce loading
1249
	 * all children with the node. Useful to refresh views after
1250
	 * state modifications, e.g. saving a form.
1251
	 *
1252
	 * @param HTTPRequest $request
1253
	 * @return string JSON
1254
	 */
1255
	public function updatetreenodes($request) {
1256
		$data = array();
1257
		$ids = explode(',', $request->getVar('ids'));
1258
		foreach($ids as $id) {
1259
			if($id === "") continue; // $id may be a blank string, which is invalid and should be skipped over
1260
1261
			$record = $this->getRecord($id);
1262
			if(!$record) continue; // In case a page is no longer available
1263
			$recordController = ($this->stat('tree_class') == 'SilverStripe\\CMS\\Model\\SiteTree')
1264
				? CMSPageEditController::singleton()
1265
				: $this;
1266
1267
			// Find the next & previous nodes, for proper positioning (Sort isn't good enough - it's not a raw offset)
1268
			// TODO: These methods should really be in hierarchy - for a start it assumes Sort exists
1269
			$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...
1270
1271
			$className = $this->stat('tree_class');
1272
			$next = DataObject::get($className)
1273
				->filter('ParentID', $record->ParentID)
1274
				->filter('Sort:GreaterThan', $record->Sort)
1275
				->first();
1276
1277
			if (!$next) {
1278
				$prev = DataObject::get($className)
1279
					->filter('ParentID', $record->ParentID)
1280
					->filter('Sort:LessThan', $record->Sort)
1281
					->reverse()
1282
					->first();
1283
			}
1284
1285
			$link = Controller::join_links($recordController->Link("show"), $record->ID);
1286
			$html = LeftAndMain_TreeNode::create($record, $link, $this->isCurrentPage($record))
1287
				->forTemplate() . '</li>';
1288
1289
			$data[$id] = array(
1290
				'html' => $html,
1291
				'ParentID' => $record->ParentID,
1292
				'NextID' => $next ? $next->ID : null,
1293
				'PrevID' => $prev ? $prev->ID : null
1294
			);
1295
		}
1296
		$this->getResponse()->addHeader('Content-Type', 'text/json');
1297
		return Convert::raw2json($data);
1298
	}
1299
1300
	/**
1301
	 * Save  handler
1302
	 *
1303
	 * @param array $data
1304
	 * @param Form $form
1305
	 * @return HTTPResponse
1306
	 */
1307
	public function save($data, $form) {
1308
		$request = $this->getRequest();
1309
		$className = $this->stat('tree_class');
1310
1311
		// Existing or new record?
1312
		$id = $data['ID'];
1313
		if(is_numeric($id) && $id > 0) {
1314
			$record = DataObject::get_by_id($className, $id);
1315
			if($record && !$record->canEdit()) {
1316
				return Security::permissionFailure($this);
1317
			}
1318
			if(!$record || !$record->ID) {
1319
				$this->httpError(404, "Bad record ID #" . (int)$id);
1320
			}
1321
		} else {
1322
			if(!singleton($this->stat('tree_class'))->canCreate()) {
1323
				return Security::permissionFailure($this);
1324
			}
1325
			$record = $this->getNewItem($id, false);
1326
		}
1327
1328
		// save form data into record
1329
		$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...
1330
		$record->write();
1331
		$this->extend('onAfterSave', $record);
1332
		$this->setCurrentPageID($record->ID);
1333
1334
		$message = _t('LeftAndMain.SAVEDUP', 'Saved.');
1335
		if($request->getHeader('X-Formschema-Request')) {
1336
			// Ensure that newly created records have all their data loaded back into the form.
1337
			$form->loadDataFrom($record);
1338
			$form->setMessage($message, 'good');
1339
			$data = $this->getSchemaForForm($form);
1340
			$response = new HTTPResponse(Convert::raw2json($data));
1341
			$response->addHeader('Content-Type', 'application/json');
1342
		} else {
1343
			$response = $this->getResponseNegotiator()->respond($request);
1344
		}
1345
1346
		$response->addHeader('X-Status', rawurlencode($message));
1347
		return $response;
1348
	}
1349
1350
	/**
1351
	 * Create new item.
1352
	 *
1353
	 * @param string|int $id
1354
	 * @param bool $setID
1355
	 * @return DataObject
1356
	 */
1357
	public function getNewItem($id, $setID = true) {
1358
		$class = $this->stat('tree_class');
1359
		$object = Injector::inst()->create($class);
1360
		if($setID) {
1361
			$object->ID = $id;
1362
		}
1363
		return $object;
1364
	}
1365
1366
	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...
1367
		$className = $this->stat('tree_class');
1368
1369
		$id = $data['ID'];
1370
		$record = DataObject::get_by_id($className, $id);
1371
		if($record && !$record->canDelete()) return Security::permissionFailure();
1372
		if(!$record || !$record->ID) $this->httpError(404, "Bad record ID #" . (int)$id);
1373
1374
		$record->delete();
1375
1376
		$this->getResponse()->addHeader('X-Status', rawurlencode(_t('LeftAndMain.DELETED', 'Deleted.')));
1377
		return $this->getResponseNegotiator()->respond(
1378
			$this->getRequest(),
1379
			array('currentform' => array($this, 'EmptyForm'))
1380
		);
1381
	}
1382
1383
	/**
1384
	 * Update the position and parent of a tree node.
1385
	 * Only saves the node if changes were made.
1386
	 *
1387
	 * Required data:
1388
	 * - 'ID': The moved node
1389
	 * - 'ParentID': New parent relation of the moved node (0 for root)
1390
	 * - 'SiblingIDs': Array of all sibling nodes to the moved node (incl. the node itself).
1391
	 *   In case of a 'ParentID' change, relates to the new siblings under the new parent.
1392
	 *
1393
	 * @param HTTPRequest $request
1394
	 * @return HTTPResponse JSON string with a
1395
	 * @throws HTTPResponse_Exception
1396
	 */
1397
	public function savetreenode($request) {
1398
		if (!SecurityToken::inst()->checkRequest($request)) {
1399
			return $this->httpError(400);
1400
		}
1401
		if (!Permission::check('SITETREE_REORGANISE') && !Permission::check('ADMIN')) {
1402
			$this->getResponse()->setStatusCode(
1403
				403,
1404
				_t('LeftAndMain.CANT_REORGANISE',
1405
					"You do not have permission to rearange the site tree. Your change was not saved.")
1406
			);
1407
			return;
1408
		}
1409
1410
		$className = $this->stat('tree_class');
1411
		$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...
1412
		$id = $request->requestVar('ID');
1413
		$parentID = $request->requestVar('ParentID');
1414
1415
		if($className == 'SilverStripe\\CMS\\Model\\SiteTree' && $page = DataObject::get_by_id('Page', $id)){
1416
			$root = $page->getParentType();
1417
			if(($parentID == '0' || $root == 'root') && !SiteConfig::current_site_config()->canCreateTopLevel()){
1418
				$this->getResponse()->setStatusCode(
1419
					403,
1420
					_t('LeftAndMain.CANT_REORGANISE',
1421
						"You do not have permission to alter Top level pages. Your change was not saved.")
1422
					);
1423
				return;
1424
			}
1425
		}
1426
1427
		$siblingIDs = $request->requestVar('SiblingIDs');
1428
		$statusUpdates = array('modified'=>array());
1429
		if(!is_numeric($id) || !is_numeric($parentID)) throw new InvalidArgumentException();
1430
1431
		$node = DataObject::get_by_id($className, $id);
1432
		if($node && !$node->canEdit()) return Security::permissionFailure($this);
1433
1434
		if(!$node) {
1435
			$this->getResponse()->setStatusCode(
1436
				500,
1437
				_t('LeftAndMain.PLEASESAVE',
1438
					"Please Save Page: This page could not be updated because it hasn't been saved yet."
1439
				)
1440
			);
1441
			return;
1442
		}
1443
1444
		// Update hierarchy (only if ParentID changed)
1445
		if($node->ParentID != $parentID) {
1446
			$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...
1447
			$node->write();
1448
1449
			$statusUpdates['modified'][$node->ID] = array(
1450
				'TreeTitle'=>$node->TreeTitle
1451
			);
1452
1453
			// Update all dependent pages
1454
			if(class_exists('SilverStripe\\CMS\\Model\\VirtualPage')) {
1455
				$virtualPages = VirtualPage::get()->filter("CopyContentFromID", $node->ID);
1456
				foreach($virtualPages as $virtualPage) {
1457
					$statusUpdates['modified'][$virtualPage->ID] = array(
1458
						'TreeTitle' => $virtualPage->TreeTitle()
1459
					);
1460
				}
1461
			}
1462
1463
			$this->getResponse()->addHeader('X-Status',
1464
				rawurlencode(_t('LeftAndMain.REORGANISATIONSUCCESSFUL', 'Reorganised the site tree successfully.')));
1465
		}
1466
1467
		// Update sorting
1468
		if(is_array($siblingIDs)) {
1469
			$counter = 0;
1470
			foreach($siblingIDs as $id) {
1471
				if($id == $node->ID) {
1472
					$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...
1473
					$node->write();
1474
					$statusUpdates['modified'][$node->ID] = array(
1475
						'TreeTitle' => $node->TreeTitle
1476
					);
1477
				} else if(is_numeric($id)) {
1478
					// Nodes that weren't "actually moved" shouldn't be registered as
1479
					// having been edited; do a direct SQL update instead
1480
					++$counter;
1481
					DB::prepared_query(
1482
						"UPDATE \"$className\" SET \"Sort\" = ? WHERE \"ID\" = ?",
1483
						array($counter, $id)
1484
					);
1485
				}
1486
			}
1487
1488
			$this->getResponse()->addHeader('X-Status',
1489
				rawurlencode(_t('LeftAndMain.REORGANISATIONSUCCESSFUL', 'Reorganised the site tree successfully.')));
1490
		}
1491
1492
		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...
1493
	}
1494
1495
	public function CanOrganiseSitetree() {
1496
		return !Permission::check('SITETREE_REORGANISE') && !Permission::check('ADMIN') ? false : true;
1497
	}
1498
1499
	/**
1500
	 * Retrieves an edit form, either for display, or to process submitted data.
1501
	 * Also used in the template rendered through {@link Right()} in the $EditForm placeholder.
1502
	 *
1503
	 * This is a "pseudo-abstract" methoed, usually connected to a {@link getEditForm()}
1504
	 * method in an entwine subclass. This method can accept a record identifier,
1505
	 * selected either in custom logic, or through {@link currentPageID()}.
1506
	 * The form usually construct itself from {@link DataObject->getCMSFields()}
1507
	 * for the specific managed subclass defined in {@link LeftAndMain::$tree_class}.
1508
	 *
1509
	 * @param HTTPRequest $request Optionally contains an identifier for the
1510
	 *  record to load into the form.
1511
	 * @return Form Should return a form regardless wether a record has been found.
1512
	 *  Form might be readonly if the current user doesn't have the permission to edit
1513
	 *  the record.
1514
	 */
1515
	/**
1516
	 * @return Form
1517
	 */
1518
	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...
1519
		return $this->getEditForm();
1520
	}
1521
1522
	/**
1523
	 * Calls {@link SiteTree->getCMSFields()}
1524
	 *
1525
	 * @param Int $id
1526
	 * @param FieldList $fields
1527
	 * @return Form
1528
	 */
1529
	public function getEditForm($id = null, $fields = null) {
1530
		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...
1531
1532
		if(is_object($id)) {
1533
			$record = $id;
1534
		} else {
1535
			$record = $this->getRecord($id);
1536
			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 1536 which is incompatible with the return type documented by SilverStripe\Admin\LeftAndMain::getEditForm of type SilverStripe\Forms\Form|null.
Loading history...
1537
		}
1538
1539
		if($record) {
1540
			$fields = ($fields) ? $fields : $record->getCMSFields();
1541
			if ($fields == null) {
1542
				user_error(
1543
					"getCMSFields() returned null  - it should return a FieldList object.
1544
					Perhaps you forgot to put a return statement at the end of your method?",
1545
					E_USER_ERROR
1546
				);
1547
			}
1548
1549
			// Add hidden fields which are required for saving the record
1550
			// and loading the UI state
1551
			if(!$fields->dataFieldByName('ClassName')) {
1552
				$fields->push(new HiddenField('ClassName'));
1553
			}
1554
1555
			$tree_class = $this->stat('tree_class');
1556
			if(
1557
				$tree_class::has_extension('SilverStripe\\ORM\\Hierarchy\\Hierarchy')
1558
				&& !$fields->dataFieldByName('ParentID')
1559
			) {
1560
				$fields->push(new HiddenField('ParentID'));
1561
			}
1562
1563
			// Added in-line to the form, but plucked into different view by frontend scripts.
1564
			if ($record instanceof CMSPreviewable) {
1565
				/** @skipUpgrade */
1566
				$navField = new LiteralField('SilverStripeNavigator', $this->getSilverStripeNavigator());
1567
				$navField->setAllowHTML(true);
1568
				$fields->push($navField);
1569
			}
1570
1571
			if($record->hasMethod('getAllCMSActions')) {
1572
				$actions = $record->getAllCMSActions();
1573
			} else {
1574
				$actions = $record->getCMSActions();
1575
				// add default actions if none are defined
1576
				if(!$actions || !$actions->count()) {
1577 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...
1578
						$actions->push(
1579
							FormAction::create('save',_t('CMSMain.SAVE','Save'))
1580
								->addExtraClass('ss-ui-action-constructive')->setAttribute('data-icon', 'accept')
1581
						);
1582
					}
1583 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...
1584
						$actions->push(
1585
							FormAction::create('delete',_t('ModelAdmin.DELETE','Delete'))
1586
								->addExtraClass('ss-ui-action-destructive')
1587
						);
1588
					}
1589
				}
1590
			}
1591
1592
			// Use <button> to allow full jQuery UI styling
1593
			$actionsFlattened = $actions->dataFields();
1594
			if($actionsFlattened) {
1595
				/** @var FormAction $action */
1596
				foreach($actionsFlattened as $action) {
1597
					$action->setUseButtonTag(true);
1598
				}
1599
			}
1600
1601
			$negotiator = $this->getResponseNegotiator();
1602
			$form = Form::create(
1603
				$this, "EditForm", $fields, $actions
1604
			)->setHTMLID('Form_EditForm');
1605
			$form->addExtraClass('cms-edit-form');
1606
			$form->loadDataFrom($record);
1607
			$form->setTemplate($this->getTemplatesWithSuffix('_EditForm'));
1608
			$form->setAttribute('data-pjax-fragment', 'CurrentForm');
1609
			$form->setValidationResponseCallback(function() use ($negotiator, $form) {
1610
				$request = $this->getRequest();
1611
				if($request->isAjax() && $negotiator) {
1612
					$form->setupFormErrors();
1613
					$result = $form->forTemplate();
1614
1615
					return $negotiator->respond($request, array(
1616
						'CurrentForm' => function() use($result) {
1617
							return $result;
1618
						}
1619
					));
1620
				}
1621
			});
1622
1623
			// Announce the capability so the frontend can decide whether to allow preview or not.
1624
			if ($record instanceof CMSPreviewable) {
1625
				$form->addExtraClass('cms-previewable');
1626
			}
1627
1628
			// Set this if you want to split up tabs into a separate header row
1629
			// if($form->Fields()->hasTabset()) {
1630
			// 	$form->Fields()->findOrMakeTab('Root')->setTemplate('SilverStripe\\Forms\\CMSTabSet');
1631
			// }
1632
1633
			// Add a default or custom validator.
1634
			// @todo Currently the default Validator.js implementation
1635
			//  adds javascript to the document body, meaning it won't
1636
			//  be included properly if the associated fields are loaded
1637
			//  through ajax. This means only serverside validation
1638
			//  will kick in for pages+validation loaded through ajax.
1639
			//  This will be solved by using less obtrusive javascript validation
1640
			//  in the future, see http://open.silverstripe.com/ticket/2915 and
1641
			//  http://open.silverstripe.com/ticket/3386
1642
			if($record->hasMethod('getCMSValidator')) {
1643
				$validator = $record->getCMSValidator();
1644
				// The clientside (mainly LeftAndMain*.js) rely on ajax responses
1645
				// which can be evaluated as javascript, hence we need
1646
				// to override any global changes to the validation handler.
1647
				if($validator != NULL){
1648
					$form->setValidator($validator);
1649
				}
1650
			} else {
1651
				$form->unsetValidator();
1652
			}
1653
1654
			if($record->hasMethod('canEdit') && !$record->canEdit()) {
1655
				$readonlyFields = $form->Fields()->makeReadonly();
1656
				$form->setFields($readonlyFields);
1657
			}
1658
		} else {
1659
			$form = $this->EmptyForm();
1660
		}
1661
1662
		return $form;
1663
	}
1664
1665
	/**
1666
	 * Returns a placeholder form, used by {@link getEditForm()} if no record is selected.
1667
	 * Our javascript logic always requires a form to be present in the CMS interface.
1668
	 *
1669
	 * @return Form
1670
	 */
1671
	public function EmptyForm() {
1672
		$form = Form::create(
1673
			$this,
1674
			"EditForm",
1675
			new FieldList(
1676
				// new HeaderField(
1677
				// 	'WelcomeHeader',
1678
				// 	$this->getApplicationName()
1679
				// ),
1680
				// new LiteralField(
1681
				// 	'WelcomeText',
1682
				// 	sprintf('<p id="WelcomeMessage">%s %s. %s</p>',
1683
				// 		_t('LeftAndMain_right_ss.WELCOMETO','Welcome to'),
1684
				// 		$this->getApplicationName(),
1685
				// 		_t('CHOOSEPAGE','Please choose an item from the left.')
1686
				// 	)
1687
				// )
1688
			),
1689
			new FieldList()
1690
		)->setHTMLID('Form_EditForm');
1691
		$form->unsetValidator();
1692
		$form->addExtraClass('cms-edit-form');
1693
		$form->addExtraClass('root-form');
1694
		$form->setTemplate($this->getTemplatesWithSuffix('_EditForm'));
1695
		$form->setAttribute('data-pjax-fragment', 'CurrentForm');
1696
1697
		return $form;
1698
	}
1699
1700
	/**
1701
	 * Return the CMS's HTML-editor toolbar
1702
	 */
1703
	public function EditorToolbar() {
1704
		return HTMLEditorField_Toolbar::create($this, "EditorToolbar");
1705
	}
1706
1707
	/**
1708
	 * Renders a panel containing tools which apply to all displayed
1709
	 * "content" (mostly through {@link EditForm()}), for example a tree navigation or a filter panel.
1710
	 * Auto-detects applicable templates by naming convention: "<controller classname>_Tools.ss",
1711
	 * and takes the most specific template (see {@link getTemplatesWithSuffix()}).
1712
	 * To explicitly disable the panel in the subclass, simply create a more specific, empty template.
1713
	 *
1714
	 * @return String HTML
1715
	 */
1716 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...
1717
		$templates = $this->getTemplatesWithSuffix('_Tools');
1718
		if($templates) {
1719
			$viewer = new SSViewer($templates);
1720
			return $viewer->process($this);
1721
		} else {
1722
			return false;
1723
		}
1724
	}
1725
1726
	/**
1727
	 * Renders a panel containing tools which apply to the currently displayed edit form.
1728
	 * The main difference to {@link Tools()} is that the panel is displayed within
1729
	 * the element structure of the form panel (rendered through {@link EditForm}).
1730
	 * This means the panel will be loaded alongside new forms, and refreshed upon save,
1731
	 * which can mean a performance hit, depending on how complex your panel logic gets.
1732
	 * Any form fields contained in the returned markup will also be submitted with the main form,
1733
	 * which might be desired depending on the implementation details.
1734
	 *
1735
	 * @return String HTML
1736
	 */
1737 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...
1738
		$templates = $this->getTemplatesWithSuffix('_EditFormTools');
1739
		if($templates) {
1740
			$viewer = new SSViewer($templates);
1741
			return $viewer->process($this);
1742
		} else {
1743
			return false;
1744
		}
1745
	}
1746
1747
	/**
1748
	 * Batch Actions Handler
1749
	 */
1750
	public function batchactions() {
1751
		return new CMSBatchActionHandler($this, 'batchactions', $this->stat('tree_class'));
1752
	}
1753
1754
	/**
1755
	 * @return Form
1756
	 */
1757
	public function BatchActionsForm() {
1758
		$actions = $this->batchactions()->batchActionList();
1759
		$actionsMap = array('-1' => _t('LeftAndMain.DropdownBatchActionsDefault', 'Choose an action...')); // Placeholder action
1760
		foreach($actions as $action) {
1761
			$actionsMap[$action->Link] = $action->Title;
1762
		}
1763
1764
		$form = new Form(
1765
			$this,
1766
			'BatchActionsForm',
1767
			new FieldList(
1768
				new HiddenField('csvIDs'),
1769
				DropdownField::create(
1770
					'Action',
1771
					false,
1772
					$actionsMap
1773
				)
1774
					->setAttribute('autocomplete', 'off')
1775
					->setAttribute('data-placeholder', _t('LeftAndMain.DropdownBatchActionsDefault', 'Choose an action...'))
1776
			),
1777
			new FieldList(
1778
				// TODO i18n
1779
				new FormAction('submit', _t('Form.SubmitBtnLabel', "Go"))
1780
			)
1781
		);
1782
		$form->addExtraClass('cms-batch-actions form--no-dividers');
1783
		$form->unsetValidator();
1784
1785
		$this->extend('updateBatchActionsForm', $form);
1786
		return $form;
1787
	}
1788
1789
	public function printable() {
1790
		$form = $this->getEditForm($this->currentPageID());
1791
		if(!$form) return false;
1792
1793
		$form->transform(new PrintableTransformation());
1794
		$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...
1795
1796
		Requirements::clear();
1797
		Requirements::css(FRAMEWORK_ADMIN_DIR . '/dist/css/LeftAndMain_printable.css');
1798
		return array(
1799
			"PrintForm" => $form
1800
		);
1801
	}
1802
1803
	/**
1804
	 * Used for preview controls, mainly links which switch between different states of the page.
1805
	 *
1806
	 * @return DBHTMLText
1807
	 */
1808
	public function getSilverStripeNavigator() {
1809
		$page = $this->currentPage();
1810
		if ($page instanceof CMSPreviewable) {
1811
			$navigator = new SilverStripeNavigator($page);
1812
			return $navigator->renderWith($this->getTemplatesWithSuffix('_SilverStripeNavigator'));
1813
		}
1814
		return null;
1815
	}
1816
1817
	/**
1818
	 * Identifier for the currently shown record,
1819
	 * in most cases a database ID. Inspects the following
1820
	 * sources (in this order):
1821
	 * - GET/POST parameter named 'ID'
1822
	 * - URL parameter named 'ID'
1823
	 * - Session value namespaced by classname, e.g. "CMSMain.currentPage"
1824
	 *
1825
	 * @return int
1826
	 */
1827
	public function currentPageID() {
1828
		if($this->getRequest()->requestVar('ID') && is_numeric($this->getRequest()->requestVar('ID')))	{
1829
			return $this->getRequest()->requestVar('ID');
1830
		} elseif ($this->getRequest()->requestVar('CMSMainCurrentPageID') && is_numeric($this->getRequest()->requestVar('CMSMainCurrentPageID'))) {
1831
			// see GridFieldDetailForm::ItemEditForm
1832
			return $this->getRequest()->requestVar('CMSMainCurrentPageID');
1833
		} elseif (isset($this->urlParams['ID']) && is_numeric($this->urlParams['ID'])) {
1834
			return $this->urlParams['ID'];
1835
		} elseif(Session::get($this->sessionNamespace() . ".currentPage")) {
1836
			return Session::get($this->sessionNamespace() . ".currentPage");
1837
		} else {
1838
			return null;
1839
		}
1840
	}
1841
1842
	/**
1843
	 * Forces the current page to be set in session,
1844
	 * which can be retrieved later through {@link currentPageID()}.
1845
	 * Keep in mind that setting an ID through GET/POST or
1846
	 * as a URL parameter will overrule this value.
1847
	 *
1848
	 * @param int $id
1849
	 */
1850
	public function setCurrentPageID($id) {
1851
		$id = (int)$id;
1852
		Session::set($this->sessionNamespace() . ".currentPage", $id);
1853
	}
1854
1855
	/**
1856
	 * Uses {@link getRecord()} and {@link currentPageID()}
1857
	 * to get the currently selected record.
1858
	 *
1859
	 * @return DataObject
1860
	 */
1861
	public function currentPage() {
1862
		return $this->getRecord($this->currentPageID());
1863
	}
1864
1865
	/**
1866
	 * Compares a given record to the currently selected one (if any).
1867
	 * Used for marking the current tree node.
1868
	 *
1869
	 * @param DataObject $record
1870
	 * @return bool
1871
	 */
1872
	public function isCurrentPage(DataObject $record) {
1873
		return ($record->ID == $this->currentPageID());
1874
	}
1875
1876
	/**
1877
	 * @return String
1878
	 */
1879
	protected function sessionNamespace() {
1880
		$override = $this->stat('session_namespace');
1881
		return $override ? $override : $this->class;
1882
	}
1883
1884
	/**
1885
	 * URL to a previewable record which is shown through this controller.
1886
	 * The controller might not have any previewable content, in which case
1887
	 * this method returns FALSE.
1888
	 *
1889
	 * @return String|boolean
1890
	 */
1891
	public function LinkPreview() {
1892
		return false;
1893
	}
1894
1895
	/**
1896
	 * Return the version number of this application.
1897
	 * Uses the number in <mymodule>/silverstripe_version
1898
	 * (automatically replaced by build scripts).
1899
	 * If silverstripe_version is empty,
1900
	 * then attempts to get it from composer.lock
1901
	 *
1902
	 * @return string
1903
	 */
1904
	public function CMSVersion() {
1905
		$versions = array();
1906
		$modules = array(
1907
			'silverstripe/framework' => array(
1908
				'title' => 'Framework',
1909
				'versionFile' => FRAMEWORK_PATH . '/silverstripe_version',
1910
			)
1911
		);
1912
		if(defined('CMS_PATH')) {
1913
			$modules['silverstripe/cms'] = array(
1914
				'title' => 'CMS',
1915
				'versionFile' => CMS_PATH . '/silverstripe_version',
1916
			);
1917
		}
1918
1919
		// Tries to obtain version number from composer.lock if it exists
1920
		$composerLockPath = BASE_PATH . '/composer.lock';
1921
		if (file_exists($composerLockPath)) {
1922
			$cache = Cache::factory('LeftAndMain_CMSVersion');
1923
			$cacheKey = filemtime($composerLockPath);
1924
			$versions = $cache->load($cacheKey);
1925
			if($versions) {
1926
				$versions = json_decode($versions, true);
1927
			} else {
1928
				$versions = array();
1929
			}
1930
			if(!$versions && $jsonData = file_get_contents($composerLockPath)) {
1931
				$lockData = json_decode($jsonData);
1932
				if($lockData && isset($lockData->packages)) {
1933
					foreach ($lockData->packages as $package) {
1934
						if(
1935
							array_key_exists($package->name, $modules)
1936
							&& isset($package->version)
1937
						) {
1938
							$versions[$package->name] = $package->version;
1939
						}
1940
					}
1941
					$cache->save(json_encode($versions), $cacheKey);
1942
				}
1943
			}
1944
		}
1945
1946
		// Fall back to static version file
1947
		foreach($modules as $moduleName => $moduleSpec) {
1948
			if(!isset($versions[$moduleName])) {
1949
				if($staticVersion = file_get_contents($moduleSpec['versionFile'])) {
1950
					$versions[$moduleName] = $staticVersion;
1951
				} else {
1952
					$versions[$moduleName] = _t('LeftAndMain.VersionUnknown', 'Unknown');
1953
				}
1954
			}
1955
		}
1956
1957
		$out = array();
1958
		foreach($modules as $moduleName => $moduleSpec) {
1959
			$out[] = $modules[$moduleName]['title'] . ': ' . $versions[$moduleName];
1960
		}
1961
		return implode(', ', $out);
1962
	}
1963
1964
	/**
1965
	 * @return array
1966
	 */
1967
	public function SwitchView() {
1968
		if($page = $this->currentPage()) {
1969
			$nav = SilverStripeNavigator::get_for_record($page);
1970
			return $nav['items'];
1971
		}
1972
	}
1973
1974
	/**
1975
	 * @return SiteConfig
1976
	 */
1977
	public function SiteConfig() {
1978
		return (class_exists('SilverStripe\\SiteConfig\\SiteConfig')) ? SiteConfig::current_site_config() : null;
1979
	}
1980
1981
	/**
1982
	 * The href for the anchor on the Silverstripe logo.
1983
	 * Set by calling LeftAndMain::set_application_link()
1984
	 *
1985
	 * @config
1986
	 * @var String
1987
	 */
1988
	private static $application_link = '//www.silverstripe.org/';
1989
1990
	/**
1991
	 * @return String
1992
	 */
1993
	public function ApplicationLink() {
1994
		return $this->stat('application_link');
1995
	}
1996
1997
	/**
1998
	 * The application name. Customisable by calling
1999
	 * LeftAndMain::setApplicationName() - the first parameter.
2000
	 *
2001
	 * @config
2002
	 * @var String
2003
	 */
2004
	private static $application_name = 'SilverStripe';
2005
2006
	/**
2007
	 * Get the application name.
2008
	 *
2009
	 * @return string
2010
	 */
2011
	public function getApplicationName() {
2012
		return $this->stat('application_name');
2013
	}
2014
2015
	/**
2016
	 * @return string
2017
	 */
2018
	public function Title() {
2019
		$app = $this->getApplicationName();
2020
2021
		return ($section = $this->SectionTitle()) ? sprintf('%s - %s', $app, $section) : $app;
2022
	}
2023
2024
	/**
2025
	 * Return the title of the current section. Either this is pulled from
2026
	 * the current panel's menu_title or from the first active menu
2027
	 *
2028
	 * @return string
2029
	 */
2030
	public function SectionTitle() {
2031
		$title = $this->menu_title();
2032
		if($title) {
2033
			return $title;
2034
		}
2035
2036
		foreach($this->MainMenu() as $menuItem) {
2037
			if($menuItem->LinkingMode != 'link') {
2038
				return $menuItem->Title;
2039
			}
2040
		}
2041
	}
2042
2043
	/**
2044
	 * Same as {@link ViewableData->CSSClasses()}, but with a changed name
2045
	 * to avoid problems when using {@link ViewableData->customise()}
2046
	 * (which always returns "ArrayData" from the $original object).
2047
	 *
2048
	 * @return String
2049
	 */
2050
	public function BaseCSSClasses() {
2051
		return $this->CSSClasses('SilverStripe\\Control\\Controller');
2052
	}
2053
2054
	/**
2055
	 * @return String
2056
	 */
2057
	public function Locale() {
2058
		return DBField::create_field('Locale', i18n::get_locale());
2059
	}
2060
2061
	public function providePermissions() {
2062
		$perms = array(
2063
			"CMS_ACCESS_LeftAndMain" => array(
2064
				'name' => _t('CMSMain.ACCESSALLINTERFACES', 'Access to all CMS sections'),
2065
				'category' => _t('Permission.CMS_ACCESS_CATEGORY', 'CMS Access'),
2066
				'help' => _t('CMSMain.ACCESSALLINTERFACESHELP', 'Overrules more specific access settings.'),
2067
				'sort' => -100
2068
			)
2069
		);
2070
2071
		// Add any custom ModelAdmin subclasses. Can't put this on ModelAdmin itself
2072
		// since its marked abstract, and needs to be singleton instanciated.
2073
		foreach(ClassInfo::subclassesFor('SilverStripe\\Admin\\ModelAdmin') as $i => $class) {
2074
			if ($class == 'SilverStripe\\Admin\\ModelAdmin') {
2075
				continue;
2076
			}
2077
			if (ClassInfo::classImplements($class, 'SilverStripe\\Dev\\TestOnly')) {
2078
				continue;
2079
			}
2080
2081
			// Check if modeladmin has explicit required_permission_codes option.
2082
			// If a modeladmin is namespaced you can apply this config to override
2083
			// the default permission generation based on fully qualified class name.
2084
			$code = $this->getRequiredPermissions();
2085
			if (!$code) {
2086
				continue;
2087
			}
2088
			// Get first permission if multiple specified
2089
			if (is_array($code)) {
2090
				$code = reset($code);
2091
			}
2092
			$title = LeftAndMain::menu_title($class);
2093
			$perms[$code] = array(
2094
				'name' => _t(
2095
					'CMSMain.ACCESS',
2096
					"Access to '{title}' section",
2097
					"Item in permission selection identifying the admin section. Example: Access to 'Files & Images'",
2098
					array('title' => $title)
2099
				),
2100
				'category' => _t('Permission.CMS_ACCESS_CATEGORY', 'CMS Access')
2101
			);
2102
		}
2103
2104
		return $perms;
2105
	}
2106
2107
}
2108