Completed
Push — master ( bbb282...43d0b8 )
by Daniel
25s
created

LeftAndMain::canView()   D

Complexity

Conditions 10
Paths 24

Size

Total Lines 32
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 1 Features 0
Metric Value
cc 10
eloc 16
nc 24
nop 1
dl 0
loc 32
rs 4.8196
c 1
b 1
f 0

How to fix   Complexity   

Long Method

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

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

Commonly applied refactorings include:

1
<?php
2
3
namespace SilverStripe\Admin;
4
5
/**
6
 * @package framework
7
 * @subpackage admin
8
 */
9
10
use SilverStripe\CMS\Controllers\CMSPageEditController;
11
use SilverStripe\CMS\Controllers\CMSPagesController;
12
use SilverStripe\Forms\Schema\FormSchema;
13
use SilverStripe\ORM\FieldType\DBHTMLText;
14
use SilverStripe\ORM\SS_List;
15
use SilverStripe\ORM\Versioning\Versioned;
16
use SilverStripe\ORM\DataModel;
17
use SilverStripe\ORM\ValidationException;
18
use SilverStripe\ORM\ArrayList;
19
use SilverStripe\ORM\FieldType\DBField;
20
use SilverStripe\ORM\DataObject;
21
use SilverStripe\ORM\DB;
22
use SilverStripe\Security\SecurityToken;
23
use SilverStripe\Security\Member;
24
use SilverStripe\Security\Permission;
25
use SilverStripe\Security\Security;
26
use SilverStripe\Security\PermissionProvider;
27
use SilverStripe\CMS\Model\SiteTree;
28
use SilverStripe\CMS\Model\VirtualPage;
29
use SilverStripe\CMS\Controllers\SilverStripeNavigator;
30
use Controller;
31
use SSViewer;
32
use Injector;
33
use Director;
34
use Convert;
35
use SS_HTTPResponse;
36
use Form;
37
use Config;
38
use i18n;
39
use Session;
40
use HTMLEditorConfig;
41
use Requirements;
42
use SS_HTTPRequest;
43
use SS_HTTPResponse_Exception;
44
use Deprecation;
45
use PjaxResponseNegotiator;
46
use ArrayData;
47
use ReflectionClass;
48
use InvalidArgumentException;
49
use SiteConfig;
50
use HiddenField;
51
use LiteralField;
52
use FormAction;
53
use FieldList;
54
use HTMLEditorField_Toolbar;
55
use DropdownField;
56
use PrintableTransformation;
57
use SS_Cache;
58
use ClassInfo;
59
use ViewableData;
60
61
62
63
64
/**
65
 * LeftAndMain is the parent class of all the two-pane views in the CMS.
66
 * If you are wanting to add more areas to the CMS, you can do it by subclassing LeftAndMain.
67
 *
68
 * This is essentially an abstract class which should be subclassed.
69
 * See {@link CMSMain} for a good example.
70
 *
71
 * @property FormSchema $schema
72
 */
73
class LeftAndMain extends Controller implements PermissionProvider {
74
75
	/**
76
	 * Enable front-end debugging (increases verbosity) in dev mode.
77
	 * Will be ignored in live environments.
78
	 *
79
	 * @var bool
80
	 */
81
	private static $client_debugging = true;
82
83
	/**
84
	 * The current url segment attached to the LeftAndMain instance
85
	 *
86
	 * @config
87
	 * @var string
88
	 */
89
	private static $url_segment;
90
91
	/**
92
	 * @config
93
	 * @var string
94
	 */
95
	private static $url_rule = '/$Action/$ID/$OtherID';
96
97
	/**
98
	 * @config
99
	 * @var string
100
	 */
101
	private static $menu_title;
102
103
	/**
104
	 * @config
105
	 * @var string
106
	 */
107
	private static $menu_icon;
108
109
	/**
110
	 * @config
111
	 * @var int
112
	 */
113
	private static $menu_priority = 0;
114
115
	/**
116
	 * @config
117
	 * @var int
118
	 */
119
	private static $url_priority = 50;
120
121
	/**
122
	 * A subclass of {@link DataObject}.
123
	 *
124
	 * Determines what is managed in this interface, through
125
	 * {@link getEditForm()} and other logic.
126
	 *
127
	 * @config
128
	 * @var string
129
	 */
130
	private static $tree_class = null;
131
132
	/**
133
	 * The url used for the link in the Help tab in the backend
134
	 *
135
	 * @config
136
	 * @var string
137
	 */
138
	private static $help_link = '//userhelp.silverstripe.org/framework/en/3.3';
139
140
	/**
141
	 * @var array
142
	 */
143
	private static $allowed_actions = [
144
		'index',
145
		'save',
146
		'savetreenode',
147
		'getsubtree',
148
		'updatetreenodes',
149
		'printable',
150
		'show',
151
		'EditorToolbar',
152
		'EditForm',
153
		'AddForm',
154
		'batchactions',
155
		'BatchActionsForm',
156
		'schema',
157
	];
158
159
	private static $url_handlers = [
160
		'GET schema/$FormName/$ItemID' => 'schema'
161
	];
162
163
	private static $dependencies = [
164
		'schema' => '%$FormSchema'
165
	];
166
167
	/**
168
	 * Assign themes to use for cms
169
	 *
170
	 * @config
171
	 * @var array
172
	 */
173
	private static $admin_themes = [
174
		'/framework/admin/themes/cms-forms',
175
		SSViewer::DEFAULT_THEME,
176
	];
177
178
	/**
179
	 * Codes which are required from the current user to view this controller.
180
	 * If multiple codes are provided, all of them are required.
181
	 * All CMS controllers require "CMS_ACCESS_LeftAndMain" as a baseline check,
182
	 * and fall back to "CMS_ACCESS_<class>" if no permissions are defined here.
183
	 * See {@link canView()} for more details on permission checks.
184
	 *
185
	 * @config
186
	 * @var array
187
	 */
188
	private static $required_permission_codes;
189
190
	/**
191
	 * @config
192
	 * @var String Namespace for session info, e.g. current record.
193
	 * Defaults to the current class name, but can be amended to share a namespace in case
194
	 * controllers are logically bundled together, and mainly separated
195
	 * to achieve more flexible templating.
196
	 */
197
	private static $session_namespace;
198
199
	/**
200
	 * Register additional requirements through the {@link Requirements} class.
201
	 * Used mainly to work around the missing "lazy loading" functionality
202
	 * for getting css/javascript required after an ajax-call (e.g. loading the editform).
203
	 *
204
	 * YAML configuration example:
205
	 * <code>
206
	 * LeftAndMain:
207
	 *   extra_requirements_javascript:
208
	 *     mysite/javascript/myscript.js:
209
	 * </code>
210
	 *
211
	 * @config
212
	 * @var array
213
	 */
214
	private static $extra_requirements_javascript = array();
215
216
	/**
217
	 * YAML configuration example:
218
	 * <code>
219
	 * LeftAndMain:
220
	 *   extra_requirements_css:
221
	 *     mysite/css/mystyle.css:
222
	 *       media: screen
223
	 * </code>
224
	 *
225
	 * @config
226
	 * @var array See {@link extra_requirements_javascript}
227
	 */
228
	private static $extra_requirements_css = array();
229
230
	/**
231
	 * @config
232
	 * @var array See {@link extra_requirements_javascript}
233
	 */
234
	private static $extra_requirements_themedCss = array();
235
236
	/**
237
	 * If true, call a keepalive ping every 5 minutes from the CMS interface,
238
	 * to ensure that the session never dies.
239
	 *
240
	 * @config
241
	 * @var boolean
242
	 */
243
	private static $session_keepalive_ping = true;
244
245
	/**
246
	 * Value of X-Frame-Options header
247
	 *
248
	 * @config
249
	 * @var string
250
	 */
251
	private static $frame_options = 'SAMEORIGIN';
252
253
	/**
254
	 * @var PjaxResponseNegotiator
255
	 */
256
	protected $responseNegotiator;
257
258
	/**
259
	 * Gets the combined configuration of all LeafAndMain subclasses required by the client app.
260
	 *
261
	 * @return array
262
	 *
263
	 * WARNING: Experimental API
264
	 */
265
	public function getCombinedClientConfig() {
266
		$combinedClientConfig = ['sections' => []];
267
		$cmsClassNames = CMSMenu::get_cms_classes('SilverStripe\\Admin\\LeftAndMain', true, CMSMenu::URL_PRIORITY);
268
269
		foreach ($cmsClassNames as $className) {
270
			$combinedClientConfig['sections'][$className] =  Injector::inst()->get($className)->getClientConfig();
271
		}
272
273
		// Pass in base url (absolute and relative)
274
		$combinedClientConfig['baseUrl'] = Director::baseURL();
275
		$combinedClientConfig['absoluteBaseUrl'] = Director::absoluteBaseURL();
276
        $combinedClientConfig['adminUrl'] = AdminRootController::admin_url();
277
278
		// Get "global" CSRF token for use in JavaScript
279
		$token = SecurityToken::inst();
280
		$combinedClientConfig[$token->getName()] = $token->getValue();
281
282
		// Set env
283
		$combinedClientConfig['environment'] = Director::get_environment_type();
284
		$combinedClientConfig['debugging'] = $this->config()->client_debugging;
0 ignored issues
show
Documentation introduced by
The property client_debugging does not exist on object<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...
285
286
		return Convert::raw2json($combinedClientConfig);
287
	}
288
289
	/**
290
	 * Returns configuration required by the client app.
291
	 *
292
	 * @return array
293
	 *
294
	 * WARNING: Experimental API
295
	 */
296
	public function getClientConfig() {
297
		return [
298
			// Trim leading/trailing slash to make it easier to concatenate URL
299
			// and use in routing definitions.
300
			'url' => trim($this->Link(), '/'),
301
		];
302
	}
303
304
	/**
305
	 * Gets a JSON schema representing the current edit form.
306
	 *
307
	 * WARNING: Experimental API.
308
	 *
309
	 * @param SS_HTTPRequest $request
310
	 * @return SS_HTTPResponse
311
	 */
312
	public function schema($request) {
313
		$response = $this->getResponse();
314
		$formName = $request->param('FormName');
315
		$itemID = $request->param('ItemID');
316
317
		if (!$formName) {
318
			return (new SS_HTTPResponse('Missing request params', 400));
319
		}
320
321
		if(!$this->hasMethod("get{$formName}")) {
322
			return (new SS_HTTPResponse('Form not found', 404));
323
		}
324
325
		if(!$this->hasAction($formName)) {
326
			return (new SS_HTTPResponse('Form not accessible', 401));
327
		}
328
329
		$form = $this->{"get{$formName}"}($itemID);
330
331
		$response->addHeader('Content-Type', 'application/json');
332
		$response->setBody(Convert::raw2json($this->getSchemaForForm($form)));
333
334
		return $response;
335
	}
336
337
	/**
338
	 * Given a form, generate a response containing the requested form
339
	 * schema if X-Formschema-Request header is set.
340
	 *
341
	 * @param Form $form
342
	 * @return SS_HTTPResponse
343
	 */
344
	protected function getSchemaResponse($form) {
345
		$request = $this->getRequest();
346
		if($request->getHeader('X-Formschema-Request')) {
347
			$data = $this->getSchemaForForm($form);
348
			$response = new SS_HTTPResponse(Convert::raw2json($data));
349
			$response->addHeader('Content-Type', 'application/json');
350
			return $response;
351
		}
352
		return null;
353
	}
354
355
	/**
356
	 * Returns a representation of the provided {@link Form} as structured data,
357
	 * based on the request data.
358
	 *
359
	 * @param Form $form
360
	 * @return array
361
	 */
362
	protected function getSchemaForForm(Form $form) {
363
		$request = $this->getRequest();
364
		$return = null;
365
366
		// Valid values for the "X-Formschema-Request" header are "schema" and "state".
367
		// If either of these values are set they will be stored in the $schemaParst array
368
		// and used to construct the response body.
369
		if ($schemaHeader = $request->getHeader('X-Formschema-Request')) {
370
			$schemaParts = array_filter(explode(',', $schemaHeader), function($value) {
371
				$validHeaderValues = ['schema', 'state'];
372
				return in_array(trim($value), $validHeaderValues);
373
			});
374
		} else {
375
			$schemaParts = ['schema'];
376
		}
377
378
		$return = ['id' => $form->FormName()];
379
380
		if (in_array('schema', $schemaParts)) {
381
			$return['schema'] = $this->schema->getSchema($form);
382
		}
383
384
		if (in_array('state', $schemaParts)) {
385
			$return['state'] = $this->schema->getState($form);
386
		}
387
388
		return $return;
389
	}
390
391
	/**
392
	 * @param Member $member
393
	 * @return boolean
394
	 */
395
	public function canView($member = null) {
396
		if(!$member && $member !== FALSE) $member = Member::currentUser();
397
398
		// cms menus only for logged-in members
399
		if(!$member) return false;
400
401
		// alternative extended checks
402
		if($this->hasMethod('alternateAccessCheck')) {
403
			$alternateAllowed = $this->alternateAccessCheck();
404
			if($alternateAllowed === false) {
405
				return false;
406
			}
407
		}
408
409
		// Check for "CMS admin" permission
410
		if(Permission::checkMember($member, "CMS_ACCESS_LeftAndMain")) {
411
			return true;
412
		}
413
414
		// Check for LeftAndMain sub-class permissions
415
		$codes = $this->getRequiredPermissions();
416
		if($codes === false) { // allow explicit FALSE to disable subclass check
417
			return true;
418
		}
419
		foreach((array)$codes as $code) {
420
			if(!Permission::checkMember($member, $code)) {
421
				return false;
422
			}
423
		}
424
425
		return true;
426
	}
427
428
	/**
429
	 * Get list of required permissions
430
	 *
431
	 * @return array|string|bool Code, array of codes, or false if no permission required
432
	 */
433
	public static function getRequiredPermissions() {
434
		$class = get_called_class();
435
		$code = Config::inst()->get($class, 'required_permission_codes', Config::FIRST_SET);
436
		if ($code === false) {
437
			return false;
438
		}
439
		if ($code) {
440
			return $code;
441
		}
442
		return "CMS_ACCESS_" . $class;
443
	}
444
445
	/**
446
	 * @uses LeftAndMainExtension->init()
447
	 * @uses LeftAndMainExtension->accessedCMS()
448
	 * @uses CMSMenu
449
	 */
450
	protected function init() {
0 ignored issues
show
Coding Style introduced by
init uses the super-global variable $_REQUEST which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

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

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
451
		parent::init();
452
453
		Config::inst()->update('SSViewer', 'rewrite_hash_links', false);
454
		Config::inst()->update('ContentNegotiator', 'enabled', false);
455
456
		// set language
457
		$member = Member::currentUser();
458
		if(!empty($member->Locale)) i18n::set_locale($member->Locale);
459
		if(!empty($member->DateFormat)) i18n::config()->date_format = $member->DateFormat;
0 ignored issues
show
Documentation introduced by
The property date_format does not exist on object<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
		if(!empty($member->TimeFormat)) i18n::config()->time_format = $member->TimeFormat;
0 ignored issues
show
Documentation introduced by
The property time_format does not exist on object<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...
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) {
0 ignored issues
show
Bug introduced by
The expression $extraJs of type array|integer|double|string|boolean is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
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
		if($extraCss) {
624
			foreach($extraCss as $file => $config) {
0 ignored issues
show
Bug introduced by
The expression $extraCss of type array|integer|double|string|boolean is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
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
		if($extraThemedCss) {
637
			foreach ($extraThemedCss as $file => $config) {
0 ignored issues
show
Bug introduced by
The expression $extraThemedCss of type array|integer|double|string|boolean is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
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(SS_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, 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 SS_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 SS_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
714
		} else {
715
			parent::redirect($url, $code);
716
		}
717
	}
718
719
	/**
720
	 * @param SS_HTTPRequest $request
721
	 * @return SS_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...
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 SS_HTTPRequest $request
821
	 * @return SS_HTTPResponse
822
	 * @throws SS_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
				foreach($menuItems as $code => $menuItem) {
886
					// alternate permission checks (in addition to LeftAndMain->canView())
887
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);
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
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);
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
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
		return SSViewer::get_templates_by_class(get_class($this), $suffix, 'SilverStripe\\Admin\\LeftAndMain');
976
	}
977
978
	public function Content() {
979
		return $this->renderWith($this->getTemplatesWithSuffix('_Content'));
980
	}
981
982
	public function getRecord($id) {
983
		$className = $this->stat('tree_class');
984
		if($className && $id instanceof $className) {
985
			return $id;
986
		} else if($className && $id == 'root') {
987
			return singleton($className);
988
		} else if($className && is_numeric($id)) {
989
			return DataObject::get_by_id($className, $id);
990
		} else {
991
			return false;
992
		}
993
	}
994
995
	/**
996
	 * @param bool $unlinked
997
	 * @return ArrayList
998
	 */
999
	public function Breadcrumbs($unlinked = false) {
1000
		$items = new ArrayList(array(
1001
			new ArrayData(array(
1002
				'Title' => $this->menu_title(),
1003
				'Link' => ($unlinked) ? false : $this->Link()
1004
			))
1005
		));
1006
		$record = $this->currentPage();
1007
		if($record && $record->exists()) {
1008
			if($record->hasExtension('SilverStripe\\ORM\\Hierarchy\\Hierarchy')) {
1009
				$ancestors = $record->getAncestors();
1010
				$ancestors = new ArrayList(array_reverse($ancestors->toArray()));
1011
				$ancestors->push($record);
1012
				foreach($ancestors as $ancestor) {
1013
					$items->push(new ArrayData(array(
1014
						'Title' => ($ancestor->MenuTitle) ? $ancestor->MenuTitle : $ancestor->Title,
1015
						'Link' => ($unlinked) ? false : Controller::join_links($this->Link('show'), $ancestor->ID)
1016
					)));
1017
				}
1018
			} else {
1019
				$items->push(new ArrayData(array(
1020
					'Title' => ($record->MenuTitle) ? $record->MenuTitle : $record->Title,
1021
					'Link' => ($unlinked) ? false : Controller::join_links($this->Link('show'), $record->ID)
1022
				)));
1023
			}
1024
		}
1025
1026
		return $items;
1027
	}
1028
1029
	/**
1030
	 * @return String HTML
1031
	 */
1032
	public function SiteTreeAsUL() {
1033
		$html = $this->getSiteTreeFor($this->stat('tree_class'));
1034
		$this->extend('updateSiteTreeAsUL', $html);
1035
		return $html;
1036
	}
1037
1038
	/**
1039
	 * Gets the current search filter for this request, if available
1040
	 *
1041
	 * @throws InvalidArgumentException
1042
	 * @return LeftAndMain_SearchFilter
1043
	 */
1044
	protected function getSearchFilter() {
1045
		// Check for given FilterClass
1046
		$params = $this->getRequest()->getVar('q');
1047
		if(empty($params['FilterClass'])) {
1048
			return null;
1049
		}
1050
1051
		// Validate classname
1052
		$filterClass = $params['FilterClass'];
1053
		$filterInfo = new ReflectionClass($filterClass);
1054
		if(!$filterInfo->implementsInterface('SilverStripe\\Admin\\LeftAndMain_SearchFilter')) {
1055
			throw new InvalidArgumentException(sprintf('Invalid filter class passed: %s', $filterClass));
1056
		}
1057
1058
		return Injector::inst()->createWithArgs($filterClass, array($params));
1059
	}
1060
1061
	/**
1062
	 * Get a site tree HTML listing which displays the nodes under the given criteria.
1063
	 *
1064
	 * @param string $className The class of the root object
1065
	 * @param string $rootID The ID of the root object.  If this is null then a complete tree will be
1066
	 *  shown
1067
	 * @param string $childrenMethod The method to call to get the children of the tree. For example,
1068
	 *  Children, AllChildrenIncludingDeleted, or AllHistoricalChildren
1069
	 * @param string $numChildrenMethod
1070
	 * @param callable $filterFunction
1071
	 * @param int $nodeCountThreshold
1072
	 * @return string Nested unordered list with links to each page
1073
	 */
1074
	public function getSiteTreeFor($className, $rootID = null, $childrenMethod = null, $numChildrenMethod = null,
1075
			$filterFunction = null, $nodeCountThreshold = 30) {
1076
1077
		// Filter criteria
1078
		$filter = $this->getSearchFilter();
1079
1080
		// Default childrenMethod and numChildrenMethod
1081
		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...
1082
			? $filter->getChildrenMethod()
1083
			: 'AllChildrenIncludingDeleted';
1084
1085
		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...
1086
			$numChildrenMethod = 'numChildren';
1087
			if($filter && $filter->getNumChildrenMethod()) {
1088
				$numChildrenMethod = $filter->getNumChildrenMethod();
1089
			}
1090
		}
1091
		if(!$filterFunction && $filter) {
1092
			$filterFunction = function($node) use($filter) {
1093
				return $filter->isPageIncluded($node);
1094
			};
1095
		}
1096
1097
		// Get the tree root
1098
		$record = ($rootID) ? $this->getRecord($rootID) : null;
1099
		$obj = $record ? $record : singleton($className);
1100
1101
		// Get the current page
1102
		// NOTE: This *must* be fetched before markPartialTree() is called, as this
1103
		// causes the Hierarchy::$marked cache to be flushed (@see CMSMain::getRecord)
1104
		// which means that deleted pages stored in the marked tree would be removed
1105
		$currentPage = $this->currentPage();
1106
1107
		// Mark the nodes of the tree to return
1108
		if ($filterFunction) $obj->setMarkingFilterFunction($filterFunction);
1109
1110
		$obj->markPartialTree($nodeCountThreshold, $this, $childrenMethod, $numChildrenMethod);
1111
1112
		// Ensure current page is exposed
1113
		if($currentPage) $obj->markToExpose($currentPage);
1114
1115
		// NOTE: SiteTree/CMSMain coupling :-(
1116
		if(class_exists('SilverStripe\\CMS\\Model\\SiteTree')) {
1117
			SiteTree::prepopulate_permission_cache(
1118
				'CanEditType',
1119
				$obj->markedNodeIDs(),
1120
				'SilverStripe\\CMS\\Model\\SiteTree::can_edit_multiple'
1121
			);
1122
		}
1123
1124
		// getChildrenAsUL is a flexible and complex way of traversing the tree
1125
		$controller = $this;
1126
		$recordController = ($this->stat('tree_class') == 'SilverStripe\\CMS\\Model\\SiteTree')
1127
			?  CMSPageEditController::singleton()
1128
			: $this;
1129
		$titleFn = function(&$child, $numChildrenMethod) use(&$controller, &$recordController, $filter) {
1130
			$link = Controller::join_links($recordController->Link("show"), $child->ID);
1131
			$node = LeftAndMain_TreeNode::create($child, $link, $controller->isCurrentPage($child), $numChildrenMethod, $filter);
1132
			return $node->forTemplate();
1133
		};
1134
1135
		// Limit the amount of nodes shown for performance reasons.
1136
		// Skip the check if we're filtering the tree, since its not clear how many children will
1137
		// match the filter criteria until they're queried (and matched up with previously marked nodes).
1138
		$nodeThresholdLeaf = Config::inst()->get('SilverStripe\\ORM\\Hierarchy\\Hierarchy', 'node_threshold_leaf');
1139
		if($nodeThresholdLeaf && !$filterFunction) {
1140
			$nodeCountCallback = function($parent, $numChildren) use(&$controller, $className, $nodeThresholdLeaf) {
1141
				if ($className !== 'SilverStripe\\CMS\\Model\\SiteTree'
1142
					|| !$parent->ID
1143
					|| $numChildren >= $nodeThresholdLeaf
1144
				) {
1145
					return null;
1146
				}
1147
					return sprintf(
1148
						'<ul><li class="readonly"><span class="item">'
1149
							. '%s (<a href="%s" class="cms-panel-link" data-pjax-target="Content">%s</a>)'
1150
							. '</span></li></ul>',
1151
						_t('LeftAndMain.TooManyPages', 'Too many pages'),
1152
						Controller::join_links(
1153
							$controller->LinkWithSearch($controller->Link()), '
1154
							?view=list&ParentID=' . $parent->ID
1155
						),
1156
						_t(
1157
							'LeftAndMain.ShowAsList',
1158
							'show as list',
1159
							'Show large amount of pages in list instead of tree view'
1160
						)
1161
					);
1162
			};
1163
		} else {
1164
			$nodeCountCallback = null;
1165
		}
1166
1167
		// If the amount of pages exceeds the node thresholds set, use the callback
1168
		$html = null;
1169
		if($obj->ParentID && $nodeCountCallback) {
1170
			$html = $nodeCountCallback($obj, $obj->$numChildrenMethod());
1171
		}
1172
1173
		// Otherwise return the actual tree (which might still filter leaf thresholds on children)
1174
		if(!$html) {
1175
			$html = $obj->getChildrenAsUL(
1176
				"",
1177
				$titleFn,
1178
				CMSPagesController::singleton(),
1179
				true,
1180
				$childrenMethod,
1181
				$numChildrenMethod,
1182
				$nodeCountThreshold,
1183
				$nodeCountCallback
1184
			);
1185
		}
1186
1187
		// Wrap the root if needs be.
1188
		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...
1189
			$rootLink = $this->Link('show') . '/root';
1190
1191
			// This lets us override the tree title with an extension
1192
			if($this->hasMethod('getCMSTreeTitle') && $customTreeTitle = $this->getCMSTreeTitle()) {
1193
				$treeTitle = $customTreeTitle;
1194
			} elseif(class_exists('SiteConfig')) {
1195
				$siteConfig = SiteConfig::current_site_config();
1196
				$treeTitle =  Convert::raw2xml($siteConfig->Title);
1197
			} else {
1198
				$treeTitle = '...';
1199
			}
1200
1201
			$html = "<ul><li id=\"record-0\" data-id=\"0\" class=\"Root nodelete\"><strong>$treeTitle</strong>"
1202
				. $html . "</li></ul>";
1203
		}
1204
1205
		return $html;
1206
	}
1207
1208
	/**
1209
	 * Get a subtree underneath the request param 'ID'.
1210
	 * If ID = 0, then get the whole tree.
1211
	 *
1212
	 * @param SS_HTTPRequest $request
1213
	 * @return string
1214
	 */
1215
	public function getsubtree($request) {
1216
		$html = $this->getSiteTreeFor(
1217
			$this->stat('tree_class'),
1218
			$request->getVar('ID'),
1219
			null,
1220
			null,
1221
			null,
1222
			$request->getVar('minNodeCount')
1223
		);
1224
1225
		// Trim off the outer tag
1226
		$html = preg_replace('/^[\s\t\r\n]*<ul[^>]*>/','', $html);
1227
		$html = preg_replace('/<\/ul[^>]*>[\s\t\r\n]*$/','', $html);
1228
1229
		return $html;
1230
	}
1231
1232
	/**
1233
	 * Allows requesting a view update on specific tree nodes.
1234
	 * Similar to {@link getsubtree()}, but doesn't enforce loading
1235
	 * all children with the node. Useful to refresh views after
1236
	 * state modifications, e.g. saving a form.
1237
	 *
1238
	 * @param SS_HTTPRequest $request
1239
	 * @return string JSON
1240
	 */
1241
	public function updatetreenodes($request) {
1242
		$data = array();
1243
		$ids = explode(',', $request->getVar('ids'));
1244
		foreach($ids as $id) {
1245
			if($id === "") continue; // $id may be a blank string, which is invalid and should be skipped over
1246
1247
			$record = $this->getRecord($id);
1248
			if(!$record) continue; // In case a page is no longer available
1249
			$recordController = ($this->stat('tree_class') == 'SilverStripe\\CMS\\Model\\SiteTree')
1250
				? CMSPageEditController::singleton()
1251
				: $this;
1252
1253
			// Find the next & previous nodes, for proper positioning (Sort isn't good enough - it's not a raw offset)
1254
			// TODO: These methods should really be in hierarchy - for a start it assumes Sort exists
1255
			$next = $prev = null;
1256
1257
			$className = $this->stat('tree_class');
1258
			$next = DataObject::get($className)
1259
				->filter('ParentID', $record->ParentID)
1260
				->filter('Sort:GreaterThan', $record->Sort)
1261
				->first();
1262
1263
			if (!$next) {
1264
				$prev = DataObject::get($className)
1265
					->filter('ParentID', $record->ParentID)
1266
					->filter('Sort:LessThan', $record->Sort)
1267
					->reverse()
1268
					->first();
1269
			}
1270
1271
			$link = Controller::join_links($recordController->Link("show"), $record->ID);
1272
			$html = LeftAndMain_TreeNode::create($record, $link, $this->isCurrentPage($record))
1273
				->forTemplate() . '</li>';
1274
1275
			$data[$id] = array(
1276
				'html' => $html,
1277
				'ParentID' => $record->ParentID,
1278
				'NextID' => $next ? $next->ID : null,
1279
				'PrevID' => $prev ? $prev->ID : null
1280
			);
1281
		}
1282
		$this->getResponse()->addHeader('Content-Type', 'text/json');
1283
		return Convert::raw2json($data);
1284
	}
1285
1286
	/**
1287
	 * Save  handler
1288
	 *
1289
	 * @param array $data
1290
	 * @param Form $form
1291
	 * @return SS_HTTPResponse
1292
	 */
1293
	public function save($data, $form) {
1294
		$request = $this->getRequest();
1295
		$className = $this->stat('tree_class');
1296
1297
		// Existing or new record?
1298
		$id = $data['ID'];
1299
		if(is_numeric($id) && $id > 0) {
1300
			$record = DataObject::get_by_id($className, $id);
1301
			if($record && !$record->canEdit()) {
1302
				return Security::permissionFailure($this);
1303
			}
1304
			if(!$record || !$record->ID) {
1305
				$this->httpError(404, "Bad record ID #" . (int)$id);
1306
			}
1307
		} else {
1308
			if(!singleton($this->stat('tree_class'))->canCreate()) {
1309
				return Security::permissionFailure($this);
1310
			}
1311
			$record = $this->getNewItem($id, false);
1312
		}
1313
1314
		// save form data into record
1315
		$form->saveInto($record, true);
1316
		$record->write();
1317
		$this->extend('onAfterSave', $record);
1318
		$this->setCurrentPageID($record->ID);
1319
1320
		$message = _t('LeftAndMain.SAVEDUP', 'Saved.');
1321
		if($request->getHeader('X-Formschema-Request')) {
1322
			// Ensure that newly created records have all their data loaded back into the form.
1323
			$form->loadDataFrom($record);
1324
			$form->setMessage($message, 'good');
1325
			$data = $this->getSchemaForForm($form);
1326
			$response = new SS_HTTPResponse(Convert::raw2json($data));
1327
			$response->addHeader('Content-Type', 'application/json');
1328
		} else {
1329
			$response = $this->getResponseNegotiator()->respond($request);
1330
		}
1331
1332
		$response->addHeader('X-Status', rawurlencode($message));
1333
		return $response;
1334
	}
1335
1336
	/**
1337
	 * Create new item.
1338
	 *
1339
	 * @param string|int $id
1340
	 * @param bool $setID
1341
	 * @return DataObject
1342
	 */
1343
	public function getNewItem($id, $setID = true) {
1344
		$class = $this->stat('tree_class');
1345
		$object = Injector::inst()->create($class);
1346
		if($setID) {
1347
			$object->ID = $id;
1348
		}
1349
		return $object;
1350
	}
1351
1352
	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...
1353
		$className = $this->stat('tree_class');
1354
1355
		$id = $data['ID'];
1356
		$record = DataObject::get_by_id($className, $id);
1357
		if($record && !$record->canDelete()) return Security::permissionFailure();
1358
		if(!$record || !$record->ID) $this->httpError(404, "Bad record ID #" . (int)$id);
1359
1360
		$record->delete();
1361
1362
		$this->getResponse()->addHeader('X-Status', rawurlencode(_t('LeftAndMain.DELETED', 'Deleted.')));
1363
		return $this->getResponseNegotiator()->respond(
1364
			$this->getRequest(),
1365
			array('currentform' => array($this, 'EmptyForm'))
1366
		);
1367
	}
1368
1369
	/**
1370
	 * Update the position and parent of a tree node.
1371
	 * Only saves the node if changes were made.
1372
	 *
1373
	 * Required data:
1374
	 * - 'ID': The moved node
1375
	 * - 'ParentID': New parent relation of the moved node (0 for root)
1376
	 * - 'SiblingIDs': Array of all sibling nodes to the moved node (incl. the node itself).
1377
	 *   In case of a 'ParentID' change, relates to the new siblings under the new parent.
1378
	 *
1379
	 * @param SS_HTTPRequest $request
1380
	 * @return SS_HTTPResponse JSON string with a
1381
	 * @throws SS_HTTPResponse_Exception
1382
	 */
1383
	public function savetreenode($request) {
1384
		if (!SecurityToken::inst()->checkRequest($request)) {
1385
			return $this->httpError(400);
1386
		}
1387
		if (!Permission::check('SITETREE_REORGANISE') && !Permission::check('ADMIN')) {
1388
			$this->getResponse()->setStatusCode(
1389
				403,
1390
				_t('LeftAndMain.CANT_REORGANISE',
1391
					"You do not have permission to rearange the site tree. Your change was not saved.")
1392
			);
1393
			return;
1394
		}
1395
1396
		$className = $this->stat('tree_class');
1397
		$statusUpdates = array('modified'=>array());
1398
		$id = $request->requestVar('ID');
1399
		$parentID = $request->requestVar('ParentID');
1400
1401
		if($className == 'SilverStripe\\CMS\\Model\\SiteTree' && $page = DataObject::get_by_id('Page', $id)){
1402
			$root = $page->getParentType();
1403
			if(($parentID == '0' || $root == 'root') && !SiteConfig::current_site_config()->canCreateTopLevel()){
1404
				$this->getResponse()->setStatusCode(
1405
					403,
1406
					_t('LeftAndMain.CANT_REORGANISE',
1407
						"You do not have permission to alter Top level pages. Your change was not saved.")
1408
					);
1409
				return;
1410
			}
1411
		}
1412
1413
		$siblingIDs = $request->requestVar('SiblingIDs');
1414
		$statusUpdates = array('modified'=>array());
1415
		if(!is_numeric($id) || !is_numeric($parentID)) throw new InvalidArgumentException();
1416
1417
		$node = DataObject::get_by_id($className, $id);
1418
		if($node && !$node->canEdit()) return Security::permissionFailure($this);
1419
1420
		if(!$node) {
1421
			$this->getResponse()->setStatusCode(
1422
				500,
1423
				_t('LeftAndMain.PLEASESAVE',
1424
					"Please Save Page: This page could not be updated because it hasn't been saved yet."
1425
				)
1426
			);
1427
			return;
1428
		}
1429
1430
		// Update hierarchy (only if ParentID changed)
1431
		if($node->ParentID != $parentID) {
1432
			$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...
1433
			$node->write();
1434
1435
			$statusUpdates['modified'][$node->ID] = array(
1436
				'TreeTitle'=>$node->TreeTitle
1437
			);
1438
1439
			// Update all dependent pages
1440
			if(class_exists('SilverStripe\\CMS\\Model\\VirtualPage')) {
1441
				$virtualPages = VirtualPage::get()->filter("CopyContentFromID", $node->ID);
1442
				foreach($virtualPages as $virtualPage) {
1443
					$statusUpdates['modified'][$virtualPage->ID] = array(
1444
						'TreeTitle' => $virtualPage->TreeTitle()
1445
					);
1446
				}
1447
			}
1448
1449
			$this->getResponse()->addHeader('X-Status',
1450
				rawurlencode(_t('LeftAndMain.REORGANISATIONSUCCESSFUL', 'Reorganised the site tree successfully.')));
1451
		}
1452
1453
		// Update sorting
1454
		if(is_array($siblingIDs)) {
1455
			$counter = 0;
1456
			foreach($siblingIDs as $id) {
1457
				if($id == $node->ID) {
1458
					$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...
1459
					$node->write();
1460
					$statusUpdates['modified'][$node->ID] = array(
1461
						'TreeTitle' => $node->TreeTitle
1462
					);
1463
				} else if(is_numeric($id)) {
1464
					// Nodes that weren't "actually moved" shouldn't be registered as
1465
					// having been edited; do a direct SQL update instead
1466
					++$counter;
1467
					DB::prepared_query(
1468
						"UPDATE \"$className\" SET \"Sort\" = ? WHERE \"ID\" = ?",
1469
						array($counter, $id)
1470
					);
1471
				}
1472
			}
1473
1474
			$this->getResponse()->addHeader('X-Status',
1475
				rawurlencode(_t('LeftAndMain.REORGANISATIONSUCCESSFUL', 'Reorganised the site tree successfully.')));
1476
		}
1477
1478
		return Convert::raw2json($statusUpdates);
1479
	}
1480
1481
	public function CanOrganiseSitetree() {
1482
		return !Permission::check('SITETREE_REORGANISE') && !Permission::check('ADMIN') ? false : true;
1483
	}
1484
1485
	/**
1486
	 * Retrieves an edit form, either for display, or to process submitted data.
1487
	 * Also used in the template rendered through {@link Right()} in the $EditForm placeholder.
1488
	 *
1489
	 * This is a "pseudo-abstract" methoed, usually connected to a {@link getEditForm()}
1490
	 * method in an entwine subclass. This method can accept a record identifier,
1491
	 * selected either in custom logic, or through {@link currentPageID()}.
1492
	 * The form usually construct itself from {@link DataObject->getCMSFields()}
1493
	 * for the specific managed subclass defined in {@link LeftAndMain::$tree_class}.
1494
	 *
1495
	 * @param SS_HTTPRequest $request Optionally contains an identifier for the
1496
	 *  record to load into the form.
1497
	 * @return Form Should return a form regardless wether a record has been found.
1498
	 *  Form might be readonly if the current user doesn't have the permission to edit
1499
	 *  the record.
1500
	 */
1501
	/**
1502
	 * @return Form
1503
	 */
1504
	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...
1505
		return $this->getEditForm();
1506
	}
1507
1508
	/**
1509
	 * Calls {@link SiteTree->getCMSFields()}
1510
	 *
1511
	 * @param Int $id
1512
	 * @param FieldList $fields
1513
	 * @return Form
1514
	 */
1515
	public function getEditForm($id = null, $fields = null) {
1516
		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...
1517
1518
		if(is_object($id)) {
1519
			$record = $id;
1520
		} else {
1521
			$record = $this->getRecord($id);
1522
			if($record && !$record->canView()) return Security::permissionFailure($this);
1523
		}
1524
1525
		if($record) {
1526
			$fields = ($fields) ? $fields : $record->getCMSFields();
1527
			if ($fields == null) {
1528
				user_error(
1529
					"getCMSFields() returned null  - it should return a FieldList object.
1530
					Perhaps you forgot to put a return statement at the end of your method?",
1531
					E_USER_ERROR
1532
				);
1533
			}
1534
1535
			// Add hidden fields which are required for saving the record
1536
			// and loading the UI state
1537
			if(!$fields->dataFieldByName('ClassName')) {
1538
				$fields->push(new HiddenField('ClassName'));
1539
			}
1540
1541
			$tree_class = $this->stat('tree_class');
1542
			if(
1543
				$tree_class::has_extension('SilverStripe\\ORM\\Hierarchy\\Hierarchy')
1544
				&& !$fields->dataFieldByName('ParentID')
1545
			) {
1546
				$fields->push(new HiddenField('ParentID'));
1547
			}
1548
1549
			// Added in-line to the form, but plucked into different view by frontend scripts.
1550
			if ($record instanceof CMSPreviewable) {
1551
				/** @skipUpgrade */
1552
				$navField = new LiteralField('SilverStripeNavigator', $this->getSilverStripeNavigator());
1553
				$navField->setAllowHTML(true);
1554
				$fields->push($navField);
1555
			}
1556
1557
			if($record->hasMethod('getAllCMSActions')) {
1558
				$actions = $record->getAllCMSActions();
1559
			} else {
1560
				$actions = $record->getCMSActions();
1561
				// add default actions if none are defined
1562
				if(!$actions || !$actions->count()) {
1563
					if($record->hasMethod('canEdit') && $record->canEdit()) {
1564
						$actions->push(
1565
							FormAction::create('save',_t('CMSMain.SAVE','Save'))
1566
								->addExtraClass('ss-ui-action-constructive')->setAttribute('data-icon', 'accept')
1567
						);
1568
					}
1569
					if($record->hasMethod('canDelete') && $record->canDelete()) {
1570
						$actions->push(
1571
							FormAction::create('delete',_t('ModelAdmin.DELETE','Delete'))
1572
								->addExtraClass('ss-ui-action-destructive')
1573
						);
1574
					}
1575
				}
1576
			}
1577
1578
			// Use <button> to allow full jQuery UI styling
1579
			$actionsFlattened = $actions->dataFields();
1580
			if($actionsFlattened) {
1581
				/** @var FormAction $action */
1582
				foreach($actionsFlattened as $action) {
1583
					$action->setUseButtonTag(true);
1584
				}
1585
			}
1586
1587
			$negotiator = $this->getResponseNegotiator();
1588
			$form = Form::create(
1589
				$this, "EditForm", $fields, $actions
1590
			)->setHTMLID('Form_EditForm');
1591
			$form->addExtraClass('cms-edit-form');
1592
			$form->loadDataFrom($record);
1593
			$form->setTemplate($this->getTemplatesWithSuffix('_EditForm'));
1594
			$form->setAttribute('data-pjax-fragment', 'CurrentForm');
1595
			$form->setValidationResponseCallback(function() use ($negotiator, $form) {
1596
				$request = $this->getRequest();
1597
				if($request->isAjax() && $negotiator) {
1598
					$form->setupFormErrors();
1599
					$result = $form->forTemplate();
1600
1601
					return $negotiator->respond($request, array(
1602
						'CurrentForm' => function() use($result) {
1603
							return $result;
1604
						}
1605
					));
1606
				}
1607
			});
1608
1609
			// Announce the capability so the frontend can decide whether to allow preview or not.
1610
			if ($record instanceof CMSPreviewable) {
1611
				$form->addExtraClass('cms-previewable');
1612
			}
1613
1614
			// Set this if you want to split up tabs into a separate header row
1615
			// if($form->Fields()->hasTabset()) {
0 ignored issues
show
Unused Code Comprehensibility introduced by
74% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
1616
			// 	$form->Fields()->findOrMakeTab('Root')->setTemplate('CMSTabSet');
0 ignored issues
show
Unused Code Comprehensibility introduced by
77% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
1617
			// }
1618
1619
			// Add a default or custom validator.
1620
			// @todo Currently the default Validator.js implementation
1621
			//  adds javascript to the document body, meaning it won't
1622
			//  be included properly if the associated fields are loaded
1623
			//  through ajax. This means only serverside validation
1624
			//  will kick in for pages+validation loaded through ajax.
1625
			//  This will be solved by using less obtrusive javascript validation
1626
			//  in the future, see http://open.silverstripe.com/ticket/2915 and
1627
			//  http://open.silverstripe.com/ticket/3386
1628
			if($record->hasMethod('getCMSValidator')) {
1629
				$validator = $record->getCMSValidator();
1630
				// The clientside (mainly LeftAndMain*.js) rely on ajax responses
1631
				// which can be evaluated as javascript, hence we need
1632
				// to override any global changes to the validation handler.
1633
				if($validator != NULL){
1634
					$form->setValidator($validator);
1635
				}
1636
			} else {
1637
				$form->unsetValidator();
1638
			}
1639
1640
			if($record->hasMethod('canEdit') && !$record->canEdit()) {
1641
				$readonlyFields = $form->Fields()->makeReadonly();
1642
				$form->setFields($readonlyFields);
1643
			}
1644
		} else {
1645
			$form = $this->EmptyForm();
1646
		}
1647
1648
		return $form;
1649
	}
1650
1651
	/**
1652
	 * Returns a placeholder form, used by {@link getEditForm()} if no record is selected.
1653
	 * Our javascript logic always requires a form to be present in the CMS interface.
1654
	 *
1655
	 * @return Form
1656
	 */
1657
	public function EmptyForm() {
1658
		$form = Form::create(
1659
			$this,
1660
			"EditForm",
1661
			new FieldList(
1662
				// new HeaderField(
1663
				// 	'WelcomeHeader',
1664
				// 	$this->getApplicationName()
0 ignored issues
show
Unused Code Comprehensibility introduced by
67% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
1665
				// ),
1666
				// new LiteralField(
1667
				// 	'WelcomeText',
1668
				// 	sprintf('<p id="WelcomeMessage">%s %s. %s</p>',
1669
				// 		_t('LeftAndMain_right_ss.WELCOMETO','Welcome to'),
0 ignored issues
show
Unused Code Comprehensibility introduced by
75% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
1670
				// 		$this->getApplicationName(),
0 ignored issues
show
Unused Code Comprehensibility introduced by
72% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
1671
				// 		_t('CHOOSEPAGE','Please choose an item from the left.')
0 ignored issues
show
Unused Code Comprehensibility introduced by
72% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
1672
				// 	)
1673
				// )
1674
			),
1675
			new FieldList()
1676
		)->setHTMLID('Form_EditForm');
1677
		$form->unsetValidator();
1678
		$form->addExtraClass('cms-edit-form');
1679
		$form->addExtraClass('root-form');
1680
		$form->setTemplate($this->getTemplatesWithSuffix('_EditForm'));
1681
		$form->setAttribute('data-pjax-fragment', 'CurrentForm');
1682
1683
		return $form;
1684
	}
1685
1686
	/**
1687
	 * Return the CMS's HTML-editor toolbar
1688
	 */
1689
	public function EditorToolbar() {
1690
		return HTMLEditorField_Toolbar::create($this, "EditorToolbar");
1691
	}
1692
1693
	/**
1694
	 * Renders a panel containing tools which apply to all displayed
1695
	 * "content" (mostly through {@link EditForm()}), for example a tree navigation or a filter panel.
1696
	 * Auto-detects applicable templates by naming convention: "<controller classname>_Tools.ss",
1697
	 * and takes the most specific template (see {@link getTemplatesWithSuffix()}).
1698
	 * To explicitly disable the panel in the subclass, simply create a more specific, empty template.
1699
	 *
1700
	 * @return String HTML
1701
	 */
1702
	public function Tools() {
1703
		$templates = $this->getTemplatesWithSuffix('_Tools');
1704
		if($templates) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $templates 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...
1705
			$viewer = new SSViewer($templates);
1706
			return $viewer->process($this);
1707
		} else {
1708
			return false;
1709
		}
1710
	}
1711
1712
	/**
1713
	 * Renders a panel containing tools which apply to the currently displayed edit form.
1714
	 * The main difference to {@link Tools()} is that the panel is displayed within
1715
	 * the element structure of the form panel (rendered through {@link EditForm}).
1716
	 * This means the panel will be loaded alongside new forms, and refreshed upon save,
1717
	 * which can mean a performance hit, depending on how complex your panel logic gets.
1718
	 * Any form fields contained in the returned markup will also be submitted with the main form,
1719
	 * which might be desired depending on the implementation details.
1720
	 *
1721
	 * @return String HTML
1722
	 */
1723
	public function EditFormTools() {
1724
		$templates = $this->getTemplatesWithSuffix('_EditFormTools');
1725
		if($templates) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $templates 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...
1726
			$viewer = new SSViewer($templates);
1727
			return $viewer->process($this);
1728
		} else {
1729
			return false;
1730
		}
1731
	}
1732
1733
	/**
1734
	 * Batch Actions Handler
1735
	 */
1736
	public function batchactions() {
1737
		return new CMSBatchActionHandler($this, 'batchactions', $this->stat('tree_class'));
1738
	}
1739
1740
	/**
1741
	 * @return Form
1742
	 */
1743
	public function BatchActionsForm() {
1744
		$actions = $this->batchactions()->batchActionList();
1745
		$actionsMap = array('-1' => _t('LeftAndMain.DropdownBatchActionsDefault', 'Choose an action...')); // Placeholder action
1746
		foreach($actions as $action) {
1747
			$actionsMap[$action->Link] = $action->Title;
1748
		}
1749
1750
		$form = new Form(
1751
			$this,
1752
			'BatchActionsForm',
1753
			new FieldList(
1754
				new HiddenField('csvIDs'),
1755
				DropdownField::create(
1756
					'Action',
1757
					false,
1758
					$actionsMap
1759
				)
1760
					->setAttribute('autocomplete', 'off')
1761
					->setAttribute('data-placeholder', _t('LeftAndMain.DropdownBatchActionsDefault', 'Choose an action...'))
1762
			),
1763
			new FieldList(
1764
				// TODO i18n
1765
				new FormAction('submit', _t('Form.SubmitBtnLabel', "Go"))
1766
			)
1767
		);
1768
		$form->addExtraClass('cms-batch-actions form--no-dividers');
1769
		$form->unsetValidator();
1770
1771
		$this->extend('updateBatchActionsForm', $form);
1772
		return $form;
1773
	}
1774
1775
	public function printable() {
1776
		$form = $this->getEditForm($this->currentPageID());
1777
		if(!$form) return false;
1778
1779
		$form->transform(new PrintableTransformation());
1780
		$form->setActions(null);
1781
1782
		Requirements::clear();
1783
		Requirements::css(FRAMEWORK_ADMIN_DIR . '/dist/css/LeftAndMain_printable.css');
1784
		return array(
1785
			"PrintForm" => $form
1786
		);
1787
	}
1788
1789
	/**
1790
	 * Used for preview controls, mainly links which switch between different states of the page.
1791
	 *
1792
	 * @return DBHTMLText
1793
	 */
1794
	public function getSilverStripeNavigator() {
1795
		$page = $this->currentPage();
1796
		if ($page instanceof CMSPreviewable) {
1797
			$navigator = new SilverStripeNavigator($page);
1798
			return $navigator->renderWith($this->getTemplatesWithSuffix('_SilverStripeNavigator'));
1799
		}
1800
		return null;
1801
	}
1802
1803
	/**
1804
	 * Identifier for the currently shown record,
1805
	 * in most cases a database ID. Inspects the following
1806
	 * sources (in this order):
1807
	 * - GET/POST parameter named 'ID'
1808
	 * - URL parameter named 'ID'
1809
	 * - Session value namespaced by classname, e.g. "CMSMain.currentPage"
1810
	 *
1811
	 * @return int
1812
	 */
1813
	public function currentPageID() {
1814
		if($this->getRequest()->requestVar('ID') && is_numeric($this->getRequest()->requestVar('ID')))	{
1815
			return $this->getRequest()->requestVar('ID');
1816
		} elseif (isset($this->urlParams['ID']) && is_numeric($this->urlParams['ID'])) {
1817
			return $this->urlParams['ID'];
1818
		} elseif(Session::get($this->sessionNamespace() . ".currentPage")) {
1819
			return Session::get($this->sessionNamespace() . ".currentPage");
1820
		} else {
1821
			return null;
1822
		}
1823
	}
1824
1825
	/**
1826
	 * Forces the current page to be set in session,
1827
	 * which can be retrieved later through {@link currentPageID()}.
1828
	 * Keep in mind that setting an ID through GET/POST or
1829
	 * as a URL parameter will overrule this value.
1830
	 *
1831
	 * @param int $id
1832
	 */
1833
	public function setCurrentPageID($id) {
1834
		$id = (int)$id;
1835
		Session::set($this->sessionNamespace() . ".currentPage", $id);
1836
	}
1837
1838
	/**
1839
	 * Uses {@link getRecord()} and {@link currentPageID()}
1840
	 * to get the currently selected record.
1841
	 *
1842
	 * @return DataObject
1843
	 */
1844
	public function currentPage() {
1845
		return $this->getRecord($this->currentPageID());
1846
	}
1847
1848
	/**
1849
	 * Compares a given record to the currently selected one (if any).
1850
	 * Used for marking the current tree node.
1851
	 *
1852
	 * @param DataObject $record
1853
	 * @return bool
1854
	 */
1855
	public function isCurrentPage(DataObject $record) {
1856
		return ($record->ID == $this->currentPageID());
1857
	}
1858
1859
	/**
1860
	 * @return String
1861
	 */
1862
	protected function sessionNamespace() {
1863
		$override = $this->stat('session_namespace');
1864
		return $override ? $override : $this->class;
1865
	}
1866
1867
	/**
1868
	 * URL to a previewable record which is shown through this controller.
1869
	 * The controller might not have any previewable content, in which case
1870
	 * this method returns FALSE.
1871
	 *
1872
	 * @return String|boolean
1873
	 */
1874
	public function LinkPreview() {
1875
		return false;
1876
	}
1877
1878
	/**
1879
	 * Return the version number of this application.
1880
	 * Uses the number in <mymodule>/silverstripe_version
1881
	 * (automatically replaced by build scripts).
1882
	 * If silverstripe_version is empty,
1883
	 * then attempts to get it from composer.lock
1884
	 *
1885
	 * @return string
1886
	 */
1887
	public function CMSVersion() {
1888
		$versions = array();
1889
		$modules = array(
1890
			'silverstripe/framework' => array(
1891
				'title' => 'Framework',
1892
				'versionFile' => FRAMEWORK_PATH . '/silverstripe_version',
1893
			)
1894
		);
1895
		if(defined('CMS_PATH')) {
1896
			$modules['silverstripe/cms'] = array(
1897
				'title' => 'CMS',
1898
				'versionFile' => CMS_PATH . '/silverstripe_version',
1899
			);
1900
		}
1901
1902
		// Tries to obtain version number from composer.lock if it exists
1903
		$composerLockPath = BASE_PATH . '/composer.lock';
1904
		if (file_exists($composerLockPath)) {
1905
			$cache = SS_Cache::factory('LeftAndMain_CMSVersion');
1906
			$cacheKey = filemtime($composerLockPath);
1907
			$versions = $cache->load($cacheKey);
1908
			if($versions) {
1909
				$versions = json_decode($versions, true);
1910
			} else {
1911
				$versions = array();
1912
			}
1913
			if(!$versions && $jsonData = file_get_contents($composerLockPath)) {
1914
				$lockData = json_decode($jsonData);
1915
				if($lockData && isset($lockData->packages)) {
1916
					foreach ($lockData->packages as $package) {
1917
						if(
1918
							array_key_exists($package->name, $modules)
1919
							&& isset($package->version)
1920
						) {
1921
							$versions[$package->name] = $package->version;
1922
						}
1923
					}
1924
					$cache->save(json_encode($versions), $cacheKey);
1925
				}
1926
			}
1927
		}
1928
1929
		// Fall back to static version file
1930
		foreach($modules as $moduleName => $moduleSpec) {
1931
			if(!isset($versions[$moduleName])) {
1932
				if($staticVersion = file_get_contents($moduleSpec['versionFile'])) {
1933
					$versions[$moduleName] = $staticVersion;
1934
				} else {
1935
					$versions[$moduleName] = _t('LeftAndMain.VersionUnknown', 'Unknown');
1936
				}
1937
			}
1938
		}
1939
1940
		$out = array();
1941
		foreach($modules as $moduleName => $moduleSpec) {
1942
			$out[] = $modules[$moduleName]['title'] . ': ' . $versions[$moduleName];
1943
		}
1944
		return implode(', ', $out);
1945
	}
1946
1947
	/**
1948
	 * @return array
1949
	 */
1950
	public function SwitchView() {
1951
		if($page = $this->currentPage()) {
1952
			$nav = SilverStripeNavigator::get_for_record($page);
1953
			return $nav['items'];
1954
		}
1955
	}
1956
1957
	/**
1958
	 * @return SiteConfig
1959
	 */
1960
	public function SiteConfig() {
1961
		return (class_exists('SiteConfig')) ? SiteConfig::current_site_config() : null;
1962
	}
1963
1964
	/**
1965
	 * The href for the anchor on the Silverstripe logo.
1966
	 * Set by calling LeftAndMain::set_application_link()
1967
	 *
1968
	 * @config
1969
	 * @var String
1970
	 */
1971
	private static $application_link = '//www.silverstripe.org/';
1972
1973
	/**
1974
	 * @return String
1975
	 */
1976
	public function ApplicationLink() {
1977
		return $this->stat('application_link');
1978
	}
1979
1980
	/**
1981
	 * The application name. Customisable by calling
1982
	 * LeftAndMain::setApplicationName() - the first parameter.
1983
	 *
1984
	 * @config
1985
	 * @var String
1986
	 */
1987
	private static $application_name = 'SilverStripe';
1988
1989
	/**
1990
	 * Get the application name.
1991
	 *
1992
	 * @return string
1993
	 */
1994
	public function getApplicationName() {
1995
		return $this->stat('application_name');
1996
	}
1997
1998
	/**
1999
	 * @return string
2000
	 */
2001
	public function Title() {
2002
		$app = $this->getApplicationName();
2003
2004
		return ($section = $this->SectionTitle()) ? sprintf('%s - %s', $app, $section) : $app;
2005
	}
2006
2007
	/**
2008
	 * Return the title of the current section. Either this is pulled from
2009
	 * the current panel's menu_title or from the first active menu
2010
	 *
2011
	 * @return string
2012
	 */
2013
	public function SectionTitle() {
2014
		$title = $this->menu_title();
2015
		if($title) {
2016
			return $title;
2017
		}
2018
2019
		foreach($this->MainMenu() as $menuItem) {
2020
			if($menuItem->LinkingMode != 'link') {
2021
				return $menuItem->Title;
2022
			}
2023
		}
2024
	}
2025
2026
	/**
2027
	 * Same as {@link ViewableData->CSSClasses()}, but with a changed name
2028
	 * to avoid problems when using {@link ViewableData->customise()}
2029
	 * (which always returns "ArrayData" from the $original object).
2030
	 *
2031
	 * @return String
2032
	 */
2033
	public function BaseCSSClasses() {
2034
		return $this->CSSClasses('Controller');
2035
	}
2036
2037
	/**
2038
	 * @return String
2039
	 */
2040
	public function Locale() {
2041
		return DBField::create_field('Locale', i18n::get_locale());
2042
	}
2043
2044
	public function providePermissions() {
2045
		$perms = array(
2046
			"CMS_ACCESS_LeftAndMain" => array(
2047
				'name' => _t('CMSMain.ACCESSALLINTERFACES', 'Access to all CMS sections'),
2048
				'category' => _t('Permission.CMS_ACCESS_CATEGORY', 'CMS Access'),
2049
				'help' => _t('CMSMain.ACCESSALLINTERFACESHELP', 'Overrules more specific access settings.'),
2050
				'sort' => -100
2051
			)
2052
		);
2053
2054
		// Add any custom ModelAdmin subclasses. Can't put this on ModelAdmin itself
2055
		// since its marked abstract, and needs to be singleton instanciated.
2056
		foreach(ClassInfo::subclassesFor('SilverStripe\\Admin\\ModelAdmin') as $i => $class) {
2057
			if ($class == 'SilverStripe\\Admin\\ModelAdmin') {
2058
				continue;
2059
			}
2060
			if (ClassInfo::classImplements($class, 'TestOnly')) {
2061
				continue;
2062
			}
2063
2064
			// Check if modeladmin has explicit required_permission_codes option.
2065
			// If a modeladmin is namespaced you can apply this config to override
2066
			// the default permission generation based on fully qualified class name.
2067
			$code = $this->getRequiredPermissions();
2068
			if (!$code) {
2069
				continue;
2070
			}
2071
			// Get first permission if multiple specified
2072
			if (is_array($code)) {
2073
				$code = reset($code);
2074
			}
2075
			$title = LeftAndMain::menu_title($class);
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
2076
			$perms[$code] = array(
2077
				'name' => _t(
2078
					'CMSMain.ACCESS',
2079
					"Access to '{title}' section",
2080
					"Item in permission selection identifying the admin section. Example: Access to 'Files & Images'",
2081
					array('title' => $title)
2082
				),
2083
				'category' => _t('Permission.CMS_ACCESS_CATEGORY', 'CMS Access')
2084
			);
2085
		}
2086
2087
		return $perms;
2088
	}
2089
2090
}
2091