Completed
Push — webpack ( cbeb9d...3103f1 )
by Ingo
08:28
created

LeftAndMain::PreviewPanel()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 4
nc 2
nop 0
dl 0
loc 7
rs 9.4285
c 0
b 0
f 0
1
<?php
2
3
namespace SilverStripe\Admin;
4
5
use SilverStripe\CMS\Controllers\CMSPageEditController;
6
use SilverStripe\CMS\Controllers\CMSPagesController;
7
use SilverStripe\CMS\Model\SiteTree;
8
use SilverStripe\CMS\Model\VirtualPage;
9
use SilverStripe\CMS\Controllers\SilverStripeNavigator;
10
use SilverStripe\Control\ContentNegotiator;
11
use SilverStripe\Control\Director;
12
use SilverStripe\Control\HTTPResponse;
13
use SilverStripe\Control\Session;
14
use SilverStripe\Control\HTTPRequest;
15
use SilverStripe\Control\HTTPResponse_Exception;
16
use SilverStripe\Control\Controller;
17
use SilverStripe\Control\PjaxResponseNegotiator;
18
use SilverStripe\Core\Convert;
19
use SilverStripe\Core\Config\Config;
20
use SilverStripe\Core\Cache;
21
use SilverStripe\Core\ClassInfo;
22
use SilverStripe\Core\Injector\Injector;
23
use SilverStripe\Dev\Deprecation;
24
use SilverStripe\Forms\Form;
25
use SilverStripe\Forms\HiddenField;
26
use SilverStripe\Forms\LiteralField;
27
use SilverStripe\Forms\FormAction;
28
use SilverStripe\Forms\FieldList;
29
use SilverStripe\Forms\DropdownField;
30
use SilverStripe\Forms\PrintableTransformation;
31
use SilverStripe\Forms\HTMLEditor\HTMLEditorConfig;
32
use SilverStripe\Forms\HTMLEditor\HTMLEditorField_Toolbar;
33
use SilverStripe\Forms\Schema\FormSchema;
34
use SilverStripe\i18n\i18n;
35
use SilverStripe\ORM\FieldType\DBHTMLText;
36
use SilverStripe\ORM\SS_List;
37
use SilverStripe\ORM\Versioning\Versioned;
38
use SilverStripe\ORM\DataModel;
39
use SilverStripe\ORM\ValidationException;
40
use SilverStripe\ORM\ArrayList;
41
use SilverStripe\ORM\FieldType\DBField;
42
use SilverStripe\ORM\DataObject;
43
use SilverStripe\ORM\DB;
44
use SilverStripe\Security\SecurityToken;
45
use SilverStripe\Security\Member;
46
use SilverStripe\Security\Permission;
47
use SilverStripe\Security\Security;
48
use SilverStripe\Security\PermissionProvider;
49
use SilverStripe\View\SSViewer;
50
use SilverStripe\View\Requirements;
51
use SilverStripe\View\ArrayData;
52
use ReflectionClass;
53
use InvalidArgumentException;
54
55
use SilverStripe\SiteConfig\SiteConfig;
56
57
58
/**
59
 * LeftAndMain is the parent class of all the two-pane views in the CMS.
60
 * If you are wanting to add more areas to the CMS, you can do it by subclassing LeftAndMain.
61
 *
62
 * This is essentially an abstract class which should be subclassed.
63
 * See {@link CMSMain} for a good example.
64
 *
65
 * @property FormSchema $schema
66
 */
67
class LeftAndMain extends Controller implements PermissionProvider {
68
69
	/**
70
	 * Enable front-end debugging (increases verbosity) in dev mode.
71
	 * Will be ignored in live environments.
72
	 *
73
	 * @var bool
74
	 */
75
	private static $client_debugging = true;
76
77
	/**
78
	 * The current url segment attached to the LeftAndMain instance
79
	 *
80
	 * @config
81
	 * @var string
82
	 */
83
	private static $url_segment;
84
85
	/**
86
	 * @config
87
	 * @var string
88
	 */
89
	private static $url_rule = '/$Action/$ID/$OtherID';
90
91
	/**
92
	 * @config
93
	 * @var string
94
	 */
95
	private static $menu_title;
96
97
	/**
98
	 * @config
99
	 * @var string
100
	 */
101
	private static $menu_icon;
102
103
	/**
104
	 * @config
105
	 * @var int
106
	 */
107
	private static $menu_priority = 0;
108
109
	/**
110
	 * @config
111
	 * @var int
112
	 */
113
	private static $url_priority = 50;
114
115
	/**
116
	 * A subclass of {@link DataObject}.
117
	 *
118
	 * Determines what is managed in this interface, through
119
	 * {@link getEditForm()} and other logic.
120
	 *
121
	 * @config
122
	 * @var string
123
	 */
124
	private static $tree_class = null;
125
126
	/**
127
	 * The url used for the link in the Help tab in the backend
128
	 *
129
	 * @config
130
	 * @var string
131
	 */
132
	private static $help_link = '//userhelp.silverstripe.org/framework/en/3.3';
133
134
	/**
135
	 * @var array
136
	 */
137
	private static $allowed_actions = [
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
138
		'index',
139
		'save',
140
		'savetreenode',
141
		'getsubtree',
142
		'updatetreenodes',
143
		'printable',
144
		'show',
145
		'EditorToolbar',
146
		'EditForm',
147
		'AddForm',
148
		'batchactions',
149
		'BatchActionsForm',
150
		'schema',
151
	];
152
153
	private static $url_handlers = [
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
154
		'GET schema/$FormName/$ItemID' => 'schema'
155
	];
156
157
	private static $dependencies = [
158
		'schema' => '%$FormSchema'
159
	];
160
161
	/**
162
	 * Assign themes to use for cms
163
	 *
164
	 * @config
165
	 * @var array
166
	 */
167
	private static $admin_themes = [
168
		'/framework/admin/themes/cms-forms',
169
		SSViewer::DEFAULT_THEME,
170
	];
171
172
	/**
173
	 * Codes which are required from the current user to view this controller.
174
	 * If multiple codes are provided, all of them are required.
175
	 * All CMS controllers require "CMS_ACCESS_LeftAndMain" as a baseline check,
176
	 * and fall back to "CMS_ACCESS_<class>" if no permissions are defined here.
177
	 * See {@link canView()} for more details on permission checks.
178
	 *
179
	 * @config
180
	 * @var array
181
	 */
182
	private static $required_permission_codes;
183
184
	/**
185
	 * @config
186
	 * @var String Namespace for session info, e.g. current record.
187
	 * Defaults to the current class name, but can be amended to share a namespace in case
188
	 * controllers are logically bundled together, and mainly separated
189
	 * to achieve more flexible templating.
190
	 */
191
	private static $session_namespace;
192
193
	/**
194
	 * Register additional requirements through the {@link Requirements} class.
195
	 * Used mainly to work around the missing "lazy loading" functionality
196
	 * for getting css/javascript required after an ajax-call (e.g. loading the editform).
197
	 *
198
	 * YAML configuration example:
199
	 * <code>
200
	 * LeftAndMain:
201
	 *   extra_requirements_javascript:
202
	 *     mysite/javascript/myscript.js:
203
	 * </code>
204
	 *
205
	 * @config
206
	 * @var array
207
	 */
208
	private static $extra_requirements_javascript = array();
209
210
	/**
211
	 * YAML configuration example:
212
	 * <code>
213
	 * LeftAndMain:
214
	 *   extra_requirements_css:
215
	 *     mysite/css/mystyle.css:
216
	 *       media: screen
217
	 * </code>
218
	 *
219
	 * @config
220
	 * @var array See {@link extra_requirements_javascript}
221
	 */
222
	private static $extra_requirements_css = array();
223
224
	/**
225
	 * @config
226
	 * @var array See {@link extra_requirements_javascript}
227
	 */
228
	private static $extra_requirements_themedCss = array();
229
230
	/**
231
	 * If true, call a keepalive ping every 5 minutes from the CMS interface,
232
	 * to ensure that the session never dies.
233
	 *
234
	 * @config
235
	 * @var boolean
236
	 */
237
	private static $session_keepalive_ping = true;
238
239
	/**
240
	 * Value of X-Frame-Options header
241
	 *
242
	 * @config
243
	 * @var string
244
	 */
245
	private static $frame_options = 'SAMEORIGIN';
246
247
	/**
248
	 * @var PjaxResponseNegotiator
249
	 */
250
	protected $responseNegotiator;
251
252
	/**
253
	 * Gets the combined configuration of all LeafAndMain subclasses required by the client app.
254
	 *
255
	 * @return array
256
	 *
257
	 * WARNING: Experimental API
258
	 */
259
	public function getCombinedClientConfig() {
260
		$combinedClientConfig = ['sections' => []];
261
		$cmsClassNames = CMSMenu::get_cms_classes('SilverStripe\\Admin\\LeftAndMain', true, CMSMenu::URL_PRIORITY);
262
263
		foreach ($cmsClassNames as $className) {
264
			$combinedClientConfig['sections'][$className] =  Injector::inst()->get($className)->getClientConfig();
265
		}
266
267
		// Pass in base url (absolute and relative)
268
		$combinedClientConfig['baseUrl'] = Director::baseURL();
269
		$combinedClientConfig['absoluteBaseUrl'] = Director::absoluteBaseURL();
270
        $combinedClientConfig['adminUrl'] = AdminRootController::admin_url();
271
272
		// Get "global" CSRF token for use in JavaScript
273
		$token = SecurityToken::inst();
274
		$combinedClientConfig[$token->getName()] = $token->getValue();
275
276
		// Set env
277
		$combinedClientConfig['environment'] = Director::get_environment_type();
278
		$combinedClientConfig['debugging'] = $this->config()->client_debugging;
0 ignored issues
show
Documentation introduced by
The property client_debugging does not exist on object<SilverStripe\Core\Config\Config_ForClass>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
279
280
		return Convert::raw2json($combinedClientConfig);
281
	}
282
283
	/**
284
	 * Returns configuration required by the client app.
285
	 *
286
	 * @return array
287
	 *
288
	 * WARNING: Experimental API
289
	 */
290
	public function getClientConfig() {
291
		return [
292
			// Trim leading/trailing slash to make it easier to concatenate URL
293
			// and use in routing definitions.
294
			'url' => trim($this->Link(), '/'),
295
		];
296
	}
297
298
	/**
299
	 * Gets a JSON schema representing the current edit form.
300
	 *
301
	 * WARNING: Experimental API.
302
	 *
303
	 * @param HTTPRequest $request
304
	 * @return HTTPResponse
305
	 */
306
	public function schema($request) {
307
		$response = $this->getResponse();
308
		$formName = $request->param('FormName');
309
		$itemID = $request->param('ItemID');
310
311
		if (!$formName) {
312
			return (new HTTPResponse('Missing request params', 400));
313
		}
314
315
		if(!$this->hasMethod("get{$formName}")) {
316
			return (new HTTPResponse('Form not found', 404));
317
		}
318
319
		if(!$this->hasAction($formName)) {
320
			return (new HTTPResponse('Form not accessible', 401));
321
		}
322
323
		$form = $this->{"get{$formName}"}($itemID);
324
325
		$response->addHeader('Content-Type', 'application/json');
326
		$response->setBody(Convert::raw2json($this->getSchemaForForm($form)));
327
328
		return $response;
329
	}
330
331
	/**
332
	 * Given a form, generate a response containing the requested form
333
	 * schema if X-Formschema-Request header is set.
334
	 *
335
	 * @param Form $form
336
	 * @return HTTPResponse
337
	 */
338
	protected function getSchemaResponse($form) {
339
		$request = $this->getRequest();
340
		if($request->getHeader('X-Formschema-Request')) {
341
			$data = $this->getSchemaForForm($form);
342
			$response = new HTTPResponse(Convert::raw2json($data));
343
			$response->addHeader('Content-Type', 'application/json');
344
			return $response;
345
		}
346
		return null;
347
	}
348
349
	/**
350
	 * Returns a representation of the provided {@link Form} as structured data,
351
	 * based on the request data.
352
	 *
353
	 * @param Form $form
354
	 * @return array
355
	 */
356
	protected function getSchemaForForm(Form $form) {
357
		$request = $this->getRequest();
358
		$return = null;
0 ignored issues
show
Unused Code introduced by
$return is not used, you could remove the assignment.

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

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

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

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

Loading history...
359
360
		// Valid values for the "X-Formschema-Request" header are "schema" and "state".
361
		// If either of these values are set they will be stored in the $schemaParst array
362
		// and used to construct the response body.
363
		if ($schemaHeader = $request->getHeader('X-Formschema-Request')) {
364
			$schemaParts = array_filter(explode(',', $schemaHeader), function($value) {
365
				$validHeaderValues = ['schema', 'state'];
366
				return in_array(trim($value), $validHeaderValues);
367
			});
368
		} else {
369
			$schemaParts = ['schema'];
370
		}
371
372
		$return = ['id' => $form->FormName()];
373
374
		if (in_array('schema', $schemaParts)) {
375
			$return['schema'] = $this->schema->getSchema($form);
376
		}
377
378
		if (in_array('state', $schemaParts)) {
379
			$return['state'] = $this->schema->getState($form);
380
		}
381
382
		return $return;
383
	}
384
385
	/**
386
	 * @param Member $member
387
	 * @return boolean
388
	 */
389
	public function canView($member = null) {
390
		if(!$member && $member !== FALSE) $member = Member::currentUser();
391
392
		// cms menus only for logged-in members
393
		if(!$member) return false;
394
395
		// alternative extended checks
396
		if($this->hasMethod('alternateAccessCheck')) {
397
			$alternateAllowed = $this->alternateAccessCheck();
398
			if($alternateAllowed === false) {
399
				return false;
400
			}
401
		}
402
403
		// Check for "CMS admin" permission
404
		if(Permission::checkMember($member, "CMS_ACCESS_LeftAndMain")) {
405
			return true;
406
		}
407
408
		// Check for LeftAndMain sub-class permissions
409
		$codes = $this->getRequiredPermissions();
410
		if($codes === false) { // allow explicit FALSE to disable subclass check
411
			return true;
412
		}
413
		foreach((array)$codes as $code) {
414
			if(!Permission::checkMember($member, $code)) {
415
				return false;
416
			}
417
		}
418
419
		return true;
420
	}
421
422
	/**
423
	 * Get list of required permissions
424
	 *
425
	 * @return array|string|bool Code, array of codes, or false if no permission required
426
	 */
427
	public static function getRequiredPermissions() {
428
		$class = get_called_class();
429
		$code = Config::inst()->get($class, 'required_permission_codes', Config::FIRST_SET);
430
		if ($code === false) {
431
			return false;
432
		}
433
		if ($code) {
434
			return $code;
435
		}
436
		return "CMS_ACCESS_" . $class;
437
	}
438
439
	/**
440
	 * @uses LeftAndMainExtension->init()
441
	 * @uses LeftAndMainExtension->accessedCMS()
442
	 * @uses CMSMenu
443
	 */
444
	protected function init() {
445
		parent::init();
446
447
		SSViewer::config()->update('rewrite_hash_links', false);
448
		ContentNegotiator::config()->update('enabled', false);
449
450
		// set language
451
		$member = Member::currentUser();
452
		if(!empty($member->Locale)) {
453
			i18n::set_locale($member->Locale);
454
		}
455
		if(!empty($member->DateFormat)) {
456
			i18n::config()->date_format = $member->DateFormat;
0 ignored issues
show
Documentation introduced by
The property date_format does not exist on object<SilverStripe\Core\Config\Config_ForClass>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
457
		}
458
		if(!empty($member->TimeFormat)) {
459
			i18n::config()->time_format = $member->TimeFormat;
0 ignored issues
show
Documentation introduced by
The property time_format does not exist on object<SilverStripe\Core\Config\Config_ForClass>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
460
		}
461
462
		// can't be done in cms/_config.php as locale is not set yet
463
		CMSMenu::add_link(
464
			'Help',
465
			_t('LeftAndMain.HELP', 'Help', 'Menu title'),
466
			$this->config()->help_link,
467
			-2,
468
			array(
469
				'target' => '_blank'
470
			)
471
		);
472
473
		// Allow customisation of the access check by a extension
474
		// Also all the canView() check to execute Controller::redirect()
475
		if(!$this->canView() && !$this->getResponse()->isFinished()) {
476
			// When access /admin/, we should try a redirect to another part of the admin rather than be locked out
477
			$menu = $this->MainMenu();
478
			foreach($menu as $candidate) {
479
				if(
480
					$candidate->Link &&
481
					$candidate->Link != $this->Link()
482
					&& $candidate->MenuItem->controller
483
					&& singleton($candidate->MenuItem->controller)->canView()
484
				) {
485
					$this->redirect($candidate->Link);
486
					return;
487
				}
488
			}
489
490
			if(Member::currentUser()) {
491
				Session::set("BackURL", null);
492
			}
493
494
			// if no alternate menu items have matched, return a permission error
495
			$messageSet = array(
496
				'default' => _t(
497
					'LeftAndMain.PERMDEFAULT',
498
					"You must be logged in to access the administration area; please enter your credentials below."
499
				),
500
				'alreadyLoggedIn' => _t(
501
					'LeftAndMain.PERMALREADY',
502
					"I'm sorry, but you can't access that part of the CMS.  If you want to log in as someone else, do"
503
					. " so below."
504
				),
505
				'logInAgain' => _t(
506
					'LeftAndMain.PERMAGAIN',
507
					"You have been logged out of the CMS.  If you would like to log in again, enter a username and"
508
					. " password below."
509
				),
510
			);
511
512
			Security::permissionFailure($this, $messageSet);
513
			return;
514
		}
515
516
		// Don't continue if there's already been a redirection request.
517
		if($this->redirectedTo()) {
518
			return;
519
		}
520
521
		// Audit logging hook
522
		if(empty($_REQUEST['executeForm']) && !$this->getRequest()->isAjax()) $this->extend('accessedCMS');
523
524
		// Set the members html editor config
525
		if(Member::currentUser()) {
526
			HTMLEditorConfig::set_active_identifier(Member::currentUser()->getHtmlEditorConfigForCMS());
527
		}
528
529
		// Set default values in the config if missing.  These things can't be defined in the config
530
		// file because insufficient information exists when that is being processed
531
		$htmlEditorConfig = HTMLEditorConfig::get_active();
532
		$htmlEditorConfig->setOption('language', i18n::get_tinymce_lang());
533
534
		Requirements::customScript("
535
			window.ss = window.ss || {};
536
			window.ss.config = " . $this->getCombinedClientConfig() . ";
537
		");
538
539
		Requirements::javascript(FRAMEWORK_ADMIN_DIR . '/client/dist/js/bundle-lib.js');
540
		Requirements::css(FRAMEWORK_ADMIN_DIR . '/client/dist/styles/bundle.css');
541
542
		Requirements::javascript(FRAMEWORK_ADMIN_DIR . '/client/dist/js/bundle-legacy.js');
543
544
		Requirements::add_i18n_javascript(FRAMEWORK_DIR . '/client/lang', false, true);
545
		Requirements::add_i18n_javascript(FRAMEWORK_ADMIN_DIR . '/client/lang', false, true);
546
547
		if ($this->config()->session_keepalive_ping) {
548
			Requirements::javascript(FRAMEWORK_ADMIN_DIR . '/client/dist/js/LeftAndMain.Ping.js');
549
		}
550
551
		if (Director::isDev()) {
552
			// TODO Confuses jQuery.ondemand through document.write()
553
			Requirements::javascript(THIRDPARTY_DIR . '/jquery-entwine/src/jquery.entwine.inspector.js');
554
			Requirements::javascript(FRAMEWORK_ADMIN_DIR . '/client/dist/js/leaktools.js');
555
		}
556
557
		Requirements::javascript(FRAMEWORK_ADMIN_DIR . '/client/dist/js/bundle-framework.js');
558
559
		// Custom requirements
560
		$extraJs = $this->stat('extra_requirements_javascript');
561
562
		if($extraJs) {
563
			foreach($extraJs as $file => $config) {
564
				if(is_numeric($file)) {
565
					$file = $config;
566
				}
567
568
				Requirements::javascript($file);
569
			}
570
		}
571
572
		$extraCss = $this->stat('extra_requirements_css');
573
574 View Code Duplication
		if($extraCss) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
575
			foreach($extraCss as $file => $config) {
576
				if(is_numeric($file)) {
577
					$file = $config;
578
					$config = array();
579
				}
580
581
				Requirements::css($file, isset($config['media']) ? $config['media'] : null);
582
			}
583
		}
584
585
		$extraThemedCss = $this->stat('extra_requirements_themedCss');
586
587 View Code Duplication
		if($extraThemedCss) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
588
			foreach ($extraThemedCss as $file => $config) {
589
				if(is_numeric($file)) {
590
					$file = $config;
591
					$config = array();
592
				}
593
594
				Requirements::themedCSS($file, isset($config['media']) ? $config['media'] : null);
595
			}
596
		}
597
598
		$dummy = null;
599
		$this->extend('init', $dummy);
600
601
		// Assign default cms theme and replace user-specified themes
602
		SSViewer::set_themes($this->config()->admin_themes);
603
604
		//set the reading mode for the admin to stage
605
		Versioned::set_stage(Versioned::DRAFT);
606
	}
607
608
	public function handleRequest(HTTPRequest $request, DataModel $model = null) {
609
		try {
610
			$response = parent::handleRequest($request, $model);
0 ignored issues
show
Bug introduced by
It seems like $model defined by parameter $model on line 608 can be null; however, SilverStripe\Control\Controller::handleRequest() does not accept null, maybe add an additional type check?

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

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

function notNullable(stdClass $x) { }

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

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

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

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

Let’s take a look at an example:

class Author {
    private $name;

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

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

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

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

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

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

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

Loading history...
665
		} else {
666
			parent::redirect($url, $code);
667
		}
668
	}
669
670
	/**
671
	 * @param HTTPRequest $request
672
	 * @return HTTPResponse
673
	 */
674
	public function index($request) {
675
		return $this->getResponseNegotiator()->respond($request);
676
	}
677
678
	/**
679
	 * If this is set to true, the "switchView" context in the
680
	 * template is shown, with links to the staging and publish site.
681
	 *
682
	 * @return boolean
683
	 */
684
	public function ShowSwitchView() {
685
		return false;
686
	}
687
688
689
	//------------------------------------------------------------------------------------------//
690
	// Main controllers
691
692
	/**
693
	 * You should implement a Link() function in your subclass of LeftAndMain,
694
	 * to point to the URL of that particular controller.
695
	 *
696
	 * @param string $action
697
	 * @return string
698
	 */
699
	public function Link($action = null) {
700
		// Handle missing url_segments
701
		if($this->config()->url_segment) {
702
			$segment = $this->config()->get('url_segment', Config::FIRST_SET);
703
		} else {
704
			$segment = $this->class;
705
		};
706
707
		$link = Controller::join_links(
708
			AdminRootController::admin_url(),
709
			$segment,
710
			'/', // trailing slash needed if $action is null!
711
			"$action"
712
		);
713
		$this->extend('updateLink', $link);
714
		return $link;
715
	}
716
717
	/**
718
	 * @deprecated 5.0
719
	 */
720
	public static function menu_title_for_class($class) {
721
		Deprecation::notice('5.0', 'Use menu_title() instead');
722
		return static::menu_title($class, false);
723
	}
724
725
	/**
726
	 * Get menu title for this section (translated)
727
	 *
728
	 * @param string $class Optional class name if called on LeftAndMain directly
729
	 * @param bool $localise Determine if menu title should be localised via i18n.
730
	 * @return string Menu title for the given class
731
	 */
732
	public static function menu_title($class = null, $localise = true) {
733
		if($class && is_subclass_of($class, __CLASS__)) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $class of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
Bug introduced by
Due to PHP Bug #53727, is_subclass_of might return inconsistent results on some PHP versions if __CLASS__ can be an interface. If so, you could instead use ReflectionClass::implementsInterface.
Loading history...
734
			// Respect oveloading of menu_title() in subclasses
735
			return $class::menu_title(null, $localise);
736
		}
737
		if(!$class) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $class of type string|null is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
738
			$class = get_called_class();
739
		}
740
741
		// Get default class title
742
		$title = Config::inst()->get($class, 'menu_title', Config::FIRST_SET);
743
		if(!$title) {
744
			$title = preg_replace('/Admin$/', '', $class);
745
		}
746
747
		// Check localisation
748
		if(!$localise) {
749
			return $title;
750
		}
751
		return i18n::_t("{$class}.MENUTITLE", $title);
752
	}
753
754
	/**
755
	 * Return styling for the menu icon, if a custom icon is set for this class
756
	 *
757
	 * Example: static $menu-icon = '/path/to/image/';
758
	 * @param string $class
759
	 * @return string
760
	 */
761
	public static function menu_icon_for_class($class) {
762
		$icon = Config::inst()->get($class, 'menu_icon', Config::FIRST_SET);
763
		if (!empty($icon)) {
764
			$class = strtolower(Convert::raw2htmlname(str_replace('\\', '-', $class)));
765
			return ".icon.icon-16.icon-{$class} { background-image: url('{$icon}'); } ";
766
		}
767
		return '';
768
	}
769
770
	/**
771
	 * @param HTTPRequest $request
772
	 * @return HTTPResponse
773
	 * @throws HTTPResponse_Exception
774
     */
775
	public function show($request) {
776
		// TODO Necessary for TableListField URLs to work properly
777
		if($request->param('ID')) $this->setCurrentPageID($request->param('ID'));
778
		return $this->getResponseNegotiator()->respond($request);
779
	}
780
781
	/**
782
	 * Caution: Volatile API.
783
	 *
784
	 * @return PjaxResponseNegotiator
785
	 */
786
	public function getResponseNegotiator() {
787
		if(!$this->responseNegotiator) {
788
			$controller = $this;
789
			$this->responseNegotiator = new PjaxResponseNegotiator(
790
				array(
791
					'CurrentForm' => function() use(&$controller) {
792
						return $controller->getEditForm()->forTemplate();
793
					},
794
					'Content' => function() use(&$controller) {
795
						return $controller->renderWith($controller->getTemplatesWithSuffix('_Content'));
796
					},
797
					'Breadcrumbs' => function() use (&$controller) {
798
						return $controller->renderWith([
799
							'type' => 'Includes',
800
							'SilverStripe\\Admin\\CMSBreadcrumbs'
801
						]);
802
					},
803
					'default' => function() use(&$controller) {
804
						return $controller->renderWith($controller->getViewer('show'));
805
					}
806
				),
807
				$this->getResponse()
808
			);
809
		}
810
		return $this->responseNegotiator;
811
	}
812
813
	//------------------------------------------------------------------------------------------//
814
	// Main UI components
815
816
	/**
817
	 * Returns the main menu of the CMS.  This is also used by init()
818
	 * to work out which sections the user has access to.
819
	 *
820
	 * @param bool $cached
821
	 * @return SS_List
822
	 */
823
	public function MainMenu($cached = true) {
824
		if(!isset($this->_cache_MainMenu) || !$cached) {
825
			// Don't accidentally return a menu if you're not logged in - it's used to determine access.
826
			if(!Member::currentUser()) return new ArrayList();
827
828
			// Encode into DO set
829
			$menu = new ArrayList();
830
			$menuItems = CMSMenu::get_viewable_menu_items();
831
832
			// extra styling for custom menu-icons
833
			$menuIconStyling = '';
834
835
			if($menuItems) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $menuItems of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
836
				/** @var CMSMenuItem $menuItem */
837
				foreach($menuItems as $code => $menuItem) {
838
					// alternate permission checks (in addition to LeftAndMain->canView())
839
					if(
840
						isset($menuItem->controller)
841
						&& $this->hasMethod('alternateMenuDisplayCheck')
842
						&& !$this->alternateMenuDisplayCheck($menuItem->controller)
843
					) {
844
						continue;
845
					}
846
847
					$linkingmode = "link";
848
849
					if($menuItem->controller && get_class($this) == $menuItem->controller) {
850
						$linkingmode = "current";
851
					} else if(strpos($this->Link(), $menuItem->url) !== false) {
852
						if($this->Link() == $menuItem->url) {
853
							$linkingmode = "current";
854
855
						// default menu is the one with a blank {@link url_segment}
856
						} else if(singleton($menuItem->controller)->stat('url_segment') == '') {
857
							if($this->Link() == AdminRootController::admin_url()) {
858
								$linkingmode = "current";
859
							}
860
861
						} else {
862
							$linkingmode = "current";
863
						}
864
					}
865
866
					// already set in CMSMenu::populate_menu(), but from a static pre-controller
867
					// context, so doesn't respect the current user locale in _t() calls - as a workaround,
868
					// we simply call LeftAndMain::menu_title() again
869
					// if we're dealing with a controller
870
					if($menuItem->controller) {
871
						$title = LeftAndMain::menu_title($menuItem->controller);
872
					} else {
873
						$title = $menuItem->title;
874
					}
875
876
					// Provide styling for custom $menu-icon. Done here instead of in
877
					// CMSMenu::populate_menu(), because the icon is part of
878
					// the CMS right pane for the specified class as well...
879
					if($menuItem->controller) {
880
						$menuIcon = LeftAndMain::menu_icon_for_class($menuItem->controller);
881
						if (!empty($menuIcon)) {
882
							$menuIconStyling .= $menuIcon;
883
						}
884
					}
885
886
					$menu->push(new ArrayData(array(
887
						"MenuItem" => $menuItem,
888
						"AttributesHTML" => $menuItem->getAttributesHTML(),
889
						"Title" => Convert::raw2xml($title),
890
						"Code" => $code,
891
						"Icon" => strtolower($code),
892
						"Link" => $menuItem->url,
893
						"LinkingMode" => $linkingmode
894
					)));
895
				}
896
			}
897
			if ($menuIconStyling) Requirements::customCSS($menuIconStyling);
898
899
			$this->_cache_MainMenu = $menu;
0 ignored issues
show
Documentation introduced by
The property _cache_MainMenu does not exist on object<SilverStripe\Admin\LeftAndMain>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
900
		}
901
902
		return $this->_cache_MainMenu;
903
	}
904
905
	public function Menu() {
906
		return $this->renderWith($this->getTemplatesWithSuffix('_Menu'));
907
	}
908
909
	/**
910
	 * @todo Wrap in CMSMenu instance accessor
911
	 * @return ArrayData A single menu entry (see {@link MainMenu})
912
	 */
913
	public function MenuCurrentItem() {
914
		$items = $this->MainMenu();
915
		return $items->find('LinkingMode', 'current');
916
	}
917
918
	/**
919
	 * Return a list of appropriate templates for this class, with the given suffix using
920
	 * {@link SSViewer::get_templates_by_class()}
921
	 *
922
	 * @param string $suffix
923
	 * @return array
924
	 */
925
	public function getTemplatesWithSuffix($suffix) {
926
		$templates = SSViewer::get_templates_by_class(get_class($this), $suffix, __CLASS__);
927
		return SSViewer::chooseTemplate($templates);
928
	}
929
930
	public function Content() {
931
		return $this->renderWith($this->getTemplatesWithSuffix('_Content'));
932
	}
933
934
	public function getRecord($id) {
935
		$className = $this->stat('tree_class');
936
		if($className && $id instanceof $className) {
937
			return $id;
938
		} else if($className && $id == 'root') {
939
			return singleton($className);
940
		} else if($className && is_numeric($id)) {
941
			return DataObject::get_by_id($className, $id);
942
		} else {
943
			return false;
944
		}
945
	}
946
947
	/**
948
	 * @param bool $unlinked
949
	 * @return ArrayList
950
	 */
951
	public function Breadcrumbs($unlinked = false) {
952
		$items = new ArrayList(array(
953
			new ArrayData(array(
954
				'Title' => $this->menu_title(),
955
				'Link' => ($unlinked) ? false : $this->Link()
956
			))
957
		));
958
		$record = $this->currentPage();
959
		if($record && $record->exists()) {
960
			if($record->hasExtension('SilverStripe\\ORM\\Hierarchy\\Hierarchy')) {
961
				$ancestors = $record->getAncestors();
962
				$ancestors = new ArrayList(array_reverse($ancestors->toArray()));
963
				$ancestors->push($record);
964
				foreach($ancestors as $ancestor) {
965
					$items->push(new ArrayData(array(
966
						'Title' => ($ancestor->MenuTitle) ? $ancestor->MenuTitle : $ancestor->Title,
967
						'Link' => ($unlinked) ? false : Controller::join_links($this->Link('show'), $ancestor->ID)
968
					)));
969
				}
970
			} else {
971
				$items->push(new ArrayData(array(
972
					'Title' => ($record->MenuTitle) ? $record->MenuTitle : $record->Title,
973
					'Link' => ($unlinked) ? false : Controller::join_links($this->Link('show'), $record->ID)
974
				)));
975
			}
976
		}
977
978
		return $items;
979
	}
980
981
	/**
982
	 * @return String HTML
983
	 */
984
	public function SiteTreeAsUL() {
985
		$html = $this->getSiteTreeFor($this->stat('tree_class'));
986
		$this->extend('updateSiteTreeAsUL', $html);
987
		return $html;
988
	}
989
990
	/**
991
	 * Gets the current search filter for this request, if available
992
	 *
993
	 * @throws InvalidArgumentException
994
	 * @return LeftAndMain_SearchFilter
995
	 */
996
	protected function getSearchFilter() {
997
		// Check for given FilterClass
998
		$params = $this->getRequest()->getVar('q');
999
		if(empty($params['FilterClass'])) {
1000
			return null;
1001
		}
1002
1003
		// Validate classname
1004
		$filterClass = $params['FilterClass'];
1005
		$filterInfo = new ReflectionClass($filterClass);
1006
		if(!$filterInfo->implementsInterface('SilverStripe\\Admin\\LeftAndMain_SearchFilter')) {
1007
			throw new InvalidArgumentException(sprintf('Invalid filter class passed: %s', $filterClass));
1008
		}
1009
1010
		return Injector::inst()->createWithArgs($filterClass, array($params));
1011
	}
1012
1013
	/**
1014
	 * Get a site tree HTML listing which displays the nodes under the given criteria.
1015
	 *
1016
	 * @param string $className The class of the root object
1017
	 * @param string $rootID The ID of the root object.  If this is null then a complete tree will be
1018
	 *  shown
1019
	 * @param string $childrenMethod The method to call to get the children of the tree. For example,
1020
	 *  Children, AllChildrenIncludingDeleted, or AllHistoricalChildren
1021
	 * @param string $numChildrenMethod
1022
	 * @param callable $filterFunction
1023
	 * @param int $nodeCountThreshold
1024
	 * @return string Nested unordered list with links to each page
1025
	 */
1026
	public function getSiteTreeFor($className, $rootID = null, $childrenMethod = null, $numChildrenMethod = null,
1027
			$filterFunction = null, $nodeCountThreshold = 30) {
1028
1029
		// Filter criteria
1030
		$filter = $this->getSearchFilter();
1031
1032
		// Default childrenMethod and numChildrenMethod
1033
		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...
1034
			? $filter->getChildrenMethod()
1035
			: 'AllChildrenIncludingDeleted';
1036
1037
		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...
1038
			$numChildrenMethod = 'numChildren';
1039
			if($filter && $filter->getNumChildrenMethod()) {
1040
				$numChildrenMethod = $filter->getNumChildrenMethod();
1041
			}
1042
		}
1043
		if(!$filterFunction && $filter) {
1044
			$filterFunction = function($node) use($filter) {
1045
				return $filter->isPageIncluded($node);
1046
			};
1047
		}
1048
1049
		// Get the tree root
1050
		$record = ($rootID) ? $this->getRecord($rootID) : null;
1051
		$obj = $record ? $record : singleton($className);
1052
1053
		// Get the current page
1054
		// NOTE: This *must* be fetched before markPartialTree() is called, as this
1055
		// causes the Hierarchy::$marked cache to be flushed (@see CMSMain::getRecord)
1056
		// which means that deleted pages stored in the marked tree would be removed
1057
		$currentPage = $this->currentPage();
1058
1059
		// Mark the nodes of the tree to return
1060
		if ($filterFunction) $obj->setMarkingFilterFunction($filterFunction);
1061
1062
		$obj->markPartialTree($nodeCountThreshold, $this, $childrenMethod, $numChildrenMethod);
1063
1064
		// Ensure current page is exposed
1065
		if($currentPage) $obj->markToExpose($currentPage);
1066
1067
		// NOTE: SiteTree/CMSMain coupling :-(
1068
		if(class_exists('SilverStripe\\CMS\\Model\\SiteTree')) {
1069
			SiteTree::prepopulate_permission_cache(
1070
				'CanEditType',
1071
				$obj->markedNodeIDs(),
1072
				'SilverStripe\\CMS\\Model\\SiteTree::can_edit_multiple'
1073
			);
1074
		}
1075
1076
		// getChildrenAsUL is a flexible and complex way of traversing the tree
1077
		$controller = $this;
1078
		$recordController = ($this->stat('tree_class') == 'SilverStripe\\CMS\\Model\\SiteTree')
1079
			?  CMSPageEditController::singleton()
1080
			: $this;
1081
		$titleFn = function(&$child, $numChildrenMethod) use(&$controller, &$recordController, $filter) {
1082
			$link = Controller::join_links($recordController->Link("show"), $child->ID);
1083
			$node = LeftAndMain_TreeNode::create($child, $link, $controller->isCurrentPage($child), $numChildrenMethod, $filter);
1084
			return $node->forTemplate();
1085
		};
1086
1087
		// Limit the amount of nodes shown for performance reasons.
1088
		// Skip the check if we're filtering the tree, since its not clear how many children will
1089
		// match the filter criteria until they're queried (and matched up with previously marked nodes).
1090
		$nodeThresholdLeaf = Config::inst()->get('SilverStripe\\ORM\\Hierarchy\\Hierarchy', 'node_threshold_leaf');
1091
		if($nodeThresholdLeaf && !$filterFunction) {
1092
			$nodeCountCallback = function($parent, $numChildren) use(&$controller, $className, $nodeThresholdLeaf) {
1093
				if ($className !== 'SilverStripe\\CMS\\Model\\SiteTree'
1094
					|| !$parent->ID
1095
					|| $numChildren >= $nodeThresholdLeaf
1096
				) {
1097
					return null;
1098
				}
1099
					return sprintf(
1100
						'<ul><li class="readonly"><span class="item">'
1101
							. '%s (<a href="%s" class="cms-panel-link" data-pjax-target="Content">%s</a>)'
1102
							. '</span></li></ul>',
1103
						_t('LeftAndMain.TooManyPages', 'Too many pages'),
1104
						Controller::join_links(
1105
							$controller->LinkWithSearch($controller->Link()), '
1106
							?view=list&ParentID=' . $parent->ID
1107
						),
1108
						_t(
1109
							'LeftAndMain.ShowAsList',
1110
							'show as list',
1111
							'Show large amount of pages in list instead of tree view'
1112
						)
1113
					);
1114
			};
1115
		} else {
1116
			$nodeCountCallback = null;
1117
		}
1118
1119
		// If the amount of pages exceeds the node thresholds set, use the callback
1120
		$html = null;
1121
		if($obj->ParentID && $nodeCountCallback) {
1122
			$html = $nodeCountCallback($obj, $obj->$numChildrenMethod());
1123
		}
1124
1125
		// Otherwise return the actual tree (which might still filter leaf thresholds on children)
1126
		if(!$html) {
1127
			$html = $obj->getChildrenAsUL(
1128
				"",
1129
				$titleFn,
1130
				CMSPagesController::singleton(),
1131
				true,
1132
				$childrenMethod,
1133
				$numChildrenMethod,
1134
				$nodeCountThreshold,
1135
				$nodeCountCallback
1136
			);
1137
		}
1138
1139
		// Wrap the root if needs be.
1140
		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...
1141
			$rootLink = $this->Link('show') . '/root';
0 ignored issues
show
Unused Code introduced by
$rootLink is not used, you could remove the assignment.

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

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

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

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

Loading history...
1142
1143
			// This lets us override the tree title with an extension
1144
			if($this->hasMethod('getCMSTreeTitle') && $customTreeTitle = $this->getCMSTreeTitle()) {
1145
				$treeTitle = $customTreeTitle;
1146
			} elseif(class_exists('SilverStripe\\SiteConfig\\SiteConfig')) {
1147
				$siteConfig = SiteConfig::current_site_config();
1148
				$treeTitle =  Convert::raw2xml($siteConfig->Title);
1149
			} else {
1150
				$treeTitle = '...';
1151
			}
1152
1153
			$html = "<ul><li id=\"record-0\" data-id=\"0\" class=\"Root nodelete\"><strong>$treeTitle</strong>"
1154
				. $html . "</li></ul>";
1155
		}
1156
1157
		return $html;
1158
	}
1159
1160
	/**
1161
	 * Get a subtree underneath the request param 'ID'.
1162
	 * If ID = 0, then get the whole tree.
1163
	 *
1164
	 * @param HTTPRequest $request
1165
	 * @return string
1166
	 */
1167
	public function getsubtree($request) {
1168
		$html = $this->getSiteTreeFor(
1169
			$this->stat('tree_class'),
1170
			$request->getVar('ID'),
1171
			null,
1172
			null,
1173
			null,
1174
			$request->getVar('minNodeCount')
1175
		);
1176
1177
		// Trim off the outer tag
1178
		$html = preg_replace('/^[\s\t\r\n]*<ul[^>]*>/','', $html);
1179
		$html = preg_replace('/<\/ul[^>]*>[\s\t\r\n]*$/','', $html);
1180
1181
		return $html;
1182
	}
1183
1184
	/**
1185
	 * Allows requesting a view update on specific tree nodes.
1186
	 * Similar to {@link getsubtree()}, but doesn't enforce loading
1187
	 * all children with the node. Useful to refresh views after
1188
	 * state modifications, e.g. saving a form.
1189
	 *
1190
	 * @param HTTPRequest $request
1191
	 * @return string JSON
1192
	 */
1193
	public function updatetreenodes($request) {
1194
		$data = array();
1195
		$ids = explode(',', $request->getVar('ids'));
1196
		foreach($ids as $id) {
1197
			if($id === "") continue; // $id may be a blank string, which is invalid and should be skipped over
1198
1199
			$record = $this->getRecord($id);
1200
			if(!$record) continue; // In case a page is no longer available
1201
			$recordController = ($this->stat('tree_class') == 'SilverStripe\\CMS\\Model\\SiteTree')
1202
				? CMSPageEditController::singleton()
1203
				: $this;
1204
1205
			// Find the next & previous nodes, for proper positioning (Sort isn't good enough - it's not a raw offset)
1206
			// TODO: These methods should really be in hierarchy - for a start it assumes Sort exists
1207
			$next = $prev = null;
0 ignored issues
show
Unused Code introduced by
$next is not used, you could remove the assignment.

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

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

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

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

Loading history...
1208
1209
			$className = $this->stat('tree_class');
1210
			$next = DataObject::get($className)
1211
				->filter('ParentID', $record->ParentID)
1212
				->filter('Sort:GreaterThan', $record->Sort)
1213
				->first();
1214
1215
			if (!$next) {
1216
				$prev = DataObject::get($className)
1217
					->filter('ParentID', $record->ParentID)
1218
					->filter('Sort:LessThan', $record->Sort)
1219
					->reverse()
1220
					->first();
1221
			}
1222
1223
			$link = Controller::join_links($recordController->Link("show"), $record->ID);
1224
			$html = LeftAndMain_TreeNode::create($record, $link, $this->isCurrentPage($record))
1225
				->forTemplate() . '</li>';
1226
1227
			$data[$id] = array(
1228
				'html' => $html,
1229
				'ParentID' => $record->ParentID,
1230
				'NextID' => $next ? $next->ID : null,
1231
				'PrevID' => $prev ? $prev->ID : null
1232
			);
1233
		}
1234
		$this->getResponse()->addHeader('Content-Type', 'text/json');
1235
		return Convert::raw2json($data);
1236
	}
1237
1238
	/**
1239
	 * Save  handler
1240
	 *
1241
	 * @param array $data
1242
	 * @param Form $form
1243
	 * @return HTTPResponse
1244
	 */
1245
	public function save($data, $form) {
1246
		$request = $this->getRequest();
1247
		$className = $this->stat('tree_class');
1248
1249
		// Existing or new record?
1250
		$id = $data['ID'];
1251
		if(is_numeric($id) && $id > 0) {
1252
			$record = DataObject::get_by_id($className, $id);
1253
			if($record && !$record->canEdit()) {
1254
				return Security::permissionFailure($this);
1255
			}
1256
			if(!$record || !$record->ID) {
1257
				$this->httpError(404, "Bad record ID #" . (int)$id);
1258
			}
1259
		} else {
1260
			if(!singleton($this->stat('tree_class'))->canCreate()) {
1261
				return Security::permissionFailure($this);
1262
			}
1263
			$record = $this->getNewItem($id, false);
1264
		}
1265
1266
		// save form data into record
1267
		$form->saveInto($record, true);
0 ignored issues
show
Documentation introduced by
true is of type boolean, but the function expects a object<SilverStripe\Forms\FieldList>|null.

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1268
		$record->write();
1269
		$this->extend('onAfterSave', $record);
1270
		$this->setCurrentPageID($record->ID);
1271
1272
		$message = _t('LeftAndMain.SAVEDUP', 'Saved.');
1273
		if($request->getHeader('X-Formschema-Request')) {
1274
			// Ensure that newly created records have all their data loaded back into the form.
1275
			$form->loadDataFrom($record);
1276
			$form->setMessage($message, 'good');
1277
			$data = $this->getSchemaForForm($form);
1278
			$response = new HTTPResponse(Convert::raw2json($data));
1279
			$response->addHeader('Content-Type', 'application/json');
1280
		} else {
1281
			$response = $this->getResponseNegotiator()->respond($request);
1282
		}
1283
1284
		$response->addHeader('X-Status', rawurlencode($message));
1285
		return $response;
1286
	}
1287
1288
	/**
1289
	 * Create new item.
1290
	 *
1291
	 * @param string|int $id
1292
	 * @param bool $setID
1293
	 * @return DataObject
1294
	 */
1295
	public function getNewItem($id, $setID = true) {
1296
		$class = $this->stat('tree_class');
1297
		$object = Injector::inst()->create($class);
1298
		if($setID) {
1299
			$object->ID = $id;
1300
		}
1301
		return $object;
1302
	}
1303
1304
	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...
1305
		$className = $this->stat('tree_class');
1306
1307
		$id = $data['ID'];
1308
		$record = DataObject::get_by_id($className, $id);
1309
		if($record && !$record->canDelete()) return Security::permissionFailure();
1310
		if(!$record || !$record->ID) $this->httpError(404, "Bad record ID #" . (int)$id);
1311
1312
		$record->delete();
1313
1314
		$this->getResponse()->addHeader('X-Status', rawurlencode(_t('LeftAndMain.DELETED', 'Deleted.')));
1315
		return $this->getResponseNegotiator()->respond(
1316
			$this->getRequest(),
1317
			array('currentform' => array($this, 'EmptyForm'))
1318
		);
1319
	}
1320
1321
	/**
1322
	 * Update the position and parent of a tree node.
1323
	 * Only saves the node if changes were made.
1324
	 *
1325
	 * Required data:
1326
	 * - 'ID': The moved node
1327
	 * - 'ParentID': New parent relation of the moved node (0 for root)
1328
	 * - 'SiblingIDs': Array of all sibling nodes to the moved node (incl. the node itself).
1329
	 *   In case of a 'ParentID' change, relates to the new siblings under the new parent.
1330
	 *
1331
	 * @param HTTPRequest $request
1332
	 * @return HTTPResponse JSON string with a
1333
	 * @throws HTTPResponse_Exception
1334
	 */
1335
	public function savetreenode($request) {
1336
		if (!SecurityToken::inst()->checkRequest($request)) {
1337
			return $this->httpError(400);
1338
		}
1339
		if (!Permission::check('SITETREE_REORGANISE') && !Permission::check('ADMIN')) {
1340
			$this->getResponse()->setStatusCode(
1341
				403,
1342
				_t('LeftAndMain.CANT_REORGANISE',
1343
					"You do not have permission to rearange the site tree. Your change was not saved.")
1344
			);
1345
			return;
1346
		}
1347
1348
		$className = $this->stat('tree_class');
1349
		$statusUpdates = array('modified'=>array());
0 ignored issues
show
Unused Code introduced by
$statusUpdates is not used, you could remove the assignment.

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

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

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

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

Loading history...
1350
		$id = $request->requestVar('ID');
1351
		$parentID = $request->requestVar('ParentID');
1352
1353
		if($className == 'SilverStripe\\CMS\\Model\\SiteTree' && $page = DataObject::get_by_id('Page', $id)){
1354
			$root = $page->getParentType();
1355
			if(($parentID == '0' || $root == 'root') && !SiteConfig::current_site_config()->canCreateTopLevel()){
1356
				$this->getResponse()->setStatusCode(
1357
					403,
1358
					_t('LeftAndMain.CANT_REORGANISE',
1359
						"You do not have permission to alter Top level pages. Your change was not saved.")
1360
					);
1361
				return;
1362
			}
1363
		}
1364
1365
		$siblingIDs = $request->requestVar('SiblingIDs');
1366
		$statusUpdates = array('modified'=>array());
1367
		if(!is_numeric($id) || !is_numeric($parentID)) throw new InvalidArgumentException();
1368
1369
		$node = DataObject::get_by_id($className, $id);
1370
		if($node && !$node->canEdit()) return Security::permissionFailure($this);
1371
1372
		if(!$node) {
1373
			$this->getResponse()->setStatusCode(
1374
				500,
1375
				_t('LeftAndMain.PLEASESAVE',
1376
					"Please Save Page: This page could not be updated because it hasn't been saved yet."
1377
				)
1378
			);
1379
			return;
1380
		}
1381
1382
		// Update hierarchy (only if ParentID changed)
1383
		if($node->ParentID != $parentID) {
1384
			$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...
1385
			$node->write();
1386
1387
			$statusUpdates['modified'][$node->ID] = array(
1388
				'TreeTitle'=>$node->TreeTitle
1389
			);
1390
1391
			// Update all dependent pages
1392
			if(class_exists('SilverStripe\\CMS\\Model\\VirtualPage')) {
1393
				$virtualPages = VirtualPage::get()->filter("CopyContentFromID", $node->ID);
1394
				foreach($virtualPages as $virtualPage) {
1395
					$statusUpdates['modified'][$virtualPage->ID] = array(
1396
						'TreeTitle' => $virtualPage->TreeTitle()
1397
					);
1398
				}
1399
			}
1400
1401
			$this->getResponse()->addHeader('X-Status',
1402
				rawurlencode(_t('LeftAndMain.REORGANISATIONSUCCESSFUL', 'Reorganised the site tree successfully.')));
1403
		}
1404
1405
		// Update sorting
1406
		if(is_array($siblingIDs)) {
1407
			$counter = 0;
1408
			foreach($siblingIDs as $id) {
1409
				if($id == $node->ID) {
1410
					$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...
1411
					$node->write();
1412
					$statusUpdates['modified'][$node->ID] = array(
1413
						'TreeTitle' => $node->TreeTitle
1414
					);
1415
				} else if(is_numeric($id)) {
1416
					// Nodes that weren't "actually moved" shouldn't be registered as
1417
					// having been edited; do a direct SQL update instead
1418
					++$counter;
1419
					DB::prepared_query(
1420
						"UPDATE \"$className\" SET \"Sort\" = ? WHERE \"ID\" = ?",
1421
						array($counter, $id)
1422
					);
1423
				}
1424
			}
1425
1426
			$this->getResponse()->addHeader('X-Status',
1427
				rawurlencode(_t('LeftAndMain.REORGANISATIONSUCCESSFUL', 'Reorganised the site tree successfully.')));
1428
		}
1429
1430
		return Convert::raw2json($statusUpdates);
0 ignored issues
show
Bug Best Practice introduced by
The return type of return \SilverStripe\Cor...w2json($statusUpdates); (string) is incompatible with the return type documented by SilverStripe\Admin\LeftAndMain::savetreenode of type SilverStripe\Control\HTTPResponse|null.

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

Let’s take a look at an example:

class Author {
    private $name;

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

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

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

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

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

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

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

Loading history...
1431
	}
1432
1433
	public function CanOrganiseSitetree() {
1434
		return !Permission::check('SITETREE_REORGANISE') && !Permission::check('ADMIN') ? false : true;
1435
	}
1436
1437
	/**
1438
	 * Retrieves an edit form, either for display, or to process submitted data.
1439
	 * Also used in the template rendered through {@link Right()} in the $EditForm placeholder.
1440
	 *
1441
	 * This is a "pseudo-abstract" methoed, usually connected to a {@link getEditForm()}
1442
	 * method in an entwine subclass. This method can accept a record identifier,
1443
	 * selected either in custom logic, or through {@link currentPageID()}.
1444
	 * The form usually construct itself from {@link DataObject->getCMSFields()}
1445
	 * for the specific managed subclass defined in {@link LeftAndMain::$tree_class}.
1446
	 *
1447
	 * @param HTTPRequest $request Optionally contains an identifier for the
1448
	 *  record to load into the form.
1449
	 * @return Form Should return a form regardless wether a record has been found.
1450
	 *  Form might be readonly if the current user doesn't have the permission to edit
1451
	 *  the record.
1452
	 */
1453
	/**
1454
	 * @return Form
1455
	 */
1456
	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...
1457
		return $this->getEditForm();
1458
	}
1459
1460
	/**
1461
	 * Calls {@link SiteTree->getCMSFields()}
1462
	 *
1463
	 * @param Int $id
1464
	 * @param FieldList $fields
1465
	 * @return Form
1466
	 */
1467
	public function getEditForm($id = null, $fields = null) {
1468
		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...
1469
1470
		if(is_object($id)) {
1471
			$record = $id;
1472
		} else {
1473
			$record = $this->getRecord($id);
1474
			if($record && !$record->canView()) return Security::permissionFailure($this);
0 ignored issues
show
Bug Compatibility introduced by
The expression \SilverStripe\Security\S...rmissionFailure($this); of type SilverStripe\Control\HTTPResponse|null adds the type SilverStripe\Control\HTTPResponse to the return on line 1474 which is incompatible with the return type documented by SilverStripe\Admin\LeftAndMain::getEditForm of type SilverStripe\Forms\Form|null.
Loading history...
1475
		}
1476
1477
		if($record) {
1478
			$fields = ($fields) ? $fields : $record->getCMSFields();
1479
			if ($fields == null) {
1480
				user_error(
1481
					"getCMSFields() returned null  - it should return a FieldList object.
1482
					Perhaps you forgot to put a return statement at the end of your method?",
1483
					E_USER_ERROR
1484
				);
1485
			}
1486
1487
			// Add hidden fields which are required for saving the record
1488
			// and loading the UI state
1489
			if(!$fields->dataFieldByName('ClassName')) {
1490
				$fields->push(new HiddenField('ClassName'));
1491
			}
1492
1493
			$tree_class = $this->stat('tree_class');
1494
			if(
1495
				$tree_class::has_extension('SilverStripe\\ORM\\Hierarchy\\Hierarchy')
1496
				&& !$fields->dataFieldByName('ParentID')
1497
			) {
1498
				$fields->push(new HiddenField('ParentID'));
1499
			}
1500
1501
			// Added in-line to the form, but plucked into different view by frontend scripts.
1502
			if ($record instanceof CMSPreviewable) {
1503
				/** @skipUpgrade */
1504
				$navField = new LiteralField('SilverStripeNavigator', $this->getSilverStripeNavigator());
1505
				$navField->setAllowHTML(true);
1506
				$fields->push($navField);
1507
			}
1508
1509
			if($record->hasMethod('getAllCMSActions')) {
1510
				$actions = $record->getAllCMSActions();
1511
			} else {
1512
				$actions = $record->getCMSActions();
1513
				// add default actions if none are defined
1514
				if(!$actions || !$actions->count()) {
1515 View Code Duplication
					if($record->hasMethod('canEdit') && $record->canEdit()) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
1516
						$actions->push(
1517
							FormAction::create('save',_t('CMSMain.SAVE','Save'))
1518
								->addExtraClass('ss-ui-action-constructive')->setAttribute('data-icon', 'accept')
1519
						);
1520
					}
1521 View Code Duplication
					if($record->hasMethod('canDelete') && $record->canDelete()) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
1522
						$actions->push(
1523
							FormAction::create('delete',_t('ModelAdmin.DELETE','Delete'))
1524
								->addExtraClass('ss-ui-action-destructive')
1525
						);
1526
					}
1527
				}
1528
			}
1529
1530
			// Use <button> to allow full jQuery UI styling
1531
			$actionsFlattened = $actions->dataFields();
1532
			if($actionsFlattened) {
1533
				/** @var FormAction $action */
1534
				foreach($actionsFlattened as $action) {
1535
					$action->setUseButtonTag(true);
1536
				}
1537
			}
1538
1539
			$negotiator = $this->getResponseNegotiator();
1540
			$form = Form::create(
1541
				$this, "EditForm", $fields, $actions
1542
			)->setHTMLID('Form_EditForm');
1543
			$form->addExtraClass('cms-edit-form');
1544
			$form->loadDataFrom($record);
1545
			$form->setTemplate($this->getTemplatesWithSuffix('_EditForm'));
1546
			$form->setAttribute('data-pjax-fragment', 'CurrentForm');
1547
			$form->setValidationResponseCallback(function() use ($negotiator, $form) {
1548
				$request = $this->getRequest();
1549
				if($request->isAjax() && $negotiator) {
1550
					$form->setupFormErrors();
1551
					$result = $form->forTemplate();
1552
1553
					return $negotiator->respond($request, array(
1554
						'CurrentForm' => function() use($result) {
1555
							return $result;
1556
						}
1557
					));
1558
				}
1559
			});
1560
1561
			// Announce the capability so the frontend can decide whether to allow preview or not.
1562
			if ($record instanceof CMSPreviewable) {
1563
				$form->addExtraClass('cms-previewable');
1564
			}
1565
1566
			// Set this if you want to split up tabs into a separate header row
1567
			// if($form->Fields()->hasTabset()) {
1568
			// 	$form->Fields()->findOrMakeTab('Root')->setTemplate('SilverStripe\\Forms\\CMSTabSet');
1569
			// }
1570
1571
			// Add a default or custom validator.
1572
			// @todo Currently the default Validator.js implementation
1573
			//  adds javascript to the document body, meaning it won't
1574
			//  be included properly if the associated fields are loaded
1575
			//  through ajax. This means only serverside validation
1576
			//  will kick in for pages+validation loaded through ajax.
1577
			//  This will be solved by using less obtrusive javascript validation
1578
			//  in the future, see http://open.silverstripe.com/ticket/2915 and
1579
			//  http://open.silverstripe.com/ticket/3386
1580
			if($record->hasMethod('getCMSValidator')) {
1581
				$validator = $record->getCMSValidator();
1582
				// The clientside (mainly LeftAndMain*.js) rely on ajax responses
1583
				// which can be evaluated as javascript, hence we need
1584
				// to override any global changes to the validation handler.
1585
				if($validator != NULL){
1586
					$form->setValidator($validator);
1587
				}
1588
			} else {
1589
				$form->unsetValidator();
1590
			}
1591
1592
			if($record->hasMethod('canEdit') && !$record->canEdit()) {
1593
				$readonlyFields = $form->Fields()->makeReadonly();
1594
				$form->setFields($readonlyFields);
1595
			}
1596
		} else {
1597
			$form = $this->EmptyForm();
1598
		}
1599
1600
		return $form;
1601
	}
1602
1603
	/**
1604
	 * Returns a placeholder form, used by {@link getEditForm()} if no record is selected.
1605
	 * Our javascript logic always requires a form to be present in the CMS interface.
1606
	 *
1607
	 * @return Form
1608
	 */
1609
	public function EmptyForm() {
1610
		$form = Form::create(
1611
			$this,
1612
			"EditForm",
1613
			new FieldList(
1614
				// new HeaderField(
1615
				// 	'WelcomeHeader',
1616
				// 	$this->getApplicationName()
1617
				// ),
1618
				// new LiteralField(
1619
				// 	'WelcomeText',
1620
				// 	sprintf('<p id="WelcomeMessage">%s %s. %s</p>',
1621
				// 		_t('LeftAndMain_right_ss.WELCOMETO','Welcome to'),
1622
				// 		$this->getApplicationName(),
1623
				// 		_t('CHOOSEPAGE','Please choose an item from the left.')
1624
				// 	)
1625
				// )
1626
			),
1627
			new FieldList()
1628
		)->setHTMLID('Form_EditForm');
1629
		$form->unsetValidator();
1630
		$form->addExtraClass('cms-edit-form');
1631
		$form->addExtraClass('root-form');
1632
		$form->setTemplate($this->getTemplatesWithSuffix('_EditForm'));
1633
		$form->setAttribute('data-pjax-fragment', 'CurrentForm');
1634
1635
		return $form;
1636
	}
1637
1638
	/**
1639
	 * Return the CMS's HTML-editor toolbar
1640
	 */
1641
	public function EditorToolbar() {
1642
		return HTMLEditorField_Toolbar::create($this, "EditorToolbar");
1643
	}
1644
1645
	/**
1646
	 * Renders a panel containing tools which apply to all displayed
1647
	 * "content" (mostly through {@link EditForm()}), for example a tree navigation or a filter panel.
1648
	 * Auto-detects applicable templates by naming convention: "<controller classname>_Tools.ss",
1649
	 * and takes the most specific template (see {@link getTemplatesWithSuffix()}).
1650
	 * To explicitly disable the panel in the subclass, simply create a more specific, empty template.
1651
	 *
1652
	 * @return String HTML
1653
	 */
1654 View Code Duplication
	public function Tools() {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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

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

Loading history...
1655
		$templates = $this->getTemplatesWithSuffix('_Tools');
1656
		if($templates) {
1657
			$viewer = new SSViewer($templates);
1658
			return $viewer->process($this);
1659
		} else {
1660
			return false;
1661
		}
1662
	}
1663
1664
	/**
1665
	 * Renders a panel containing tools which apply to the currently displayed edit form.
1666
	 * The main difference to {@link Tools()} is that the panel is displayed within
1667
	 * the element structure of the form panel (rendered through {@link EditForm}).
1668
	 * This means the panel will be loaded alongside new forms, and refreshed upon save,
1669
	 * which can mean a performance hit, depending on how complex your panel logic gets.
1670
	 * Any form fields contained in the returned markup will also be submitted with the main form,
1671
	 * which might be desired depending on the implementation details.
1672
	 *
1673
	 * @return String HTML
1674
	 */
1675 View Code Duplication
	public function EditFormTools() {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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

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

Loading history...
1676
		$templates = $this->getTemplatesWithSuffix('_EditFormTools');
1677
		if($templates) {
1678
			$viewer = new SSViewer($templates);
1679
			return $viewer->process($this);
1680
		} else {
1681
			return false;
1682
		}
1683
	}
1684
1685
	/**
1686
	 * Batch Actions Handler
1687
	 */
1688
	public function batchactions() {
1689
		return new CMSBatchActionHandler($this, 'batchactions', $this->stat('tree_class'));
1690
	}
1691
1692
	/**
1693
	 * @return Form
1694
	 */
1695
	public function BatchActionsForm() {
1696
		$actions = $this->batchactions()->batchActionList();
1697
		$actionsMap = array('-1' => _t('LeftAndMain.DropdownBatchActionsDefault', 'Choose an action...')); // Placeholder action
1698
		foreach($actions as $action) {
1699
			$actionsMap[$action->Link] = $action->Title;
1700
		}
1701
1702
		$form = new Form(
1703
			$this,
1704
			'BatchActionsForm',
1705
			new FieldList(
1706
				new HiddenField('csvIDs'),
1707
				DropdownField::create(
1708
					'Action',
1709
					false,
1710
					$actionsMap
1711
				)
1712
					->setAttribute('autocomplete', 'off')
1713
					->setAttribute('data-placeholder', _t('LeftAndMain.DropdownBatchActionsDefault', 'Choose an action...'))
1714
			),
1715
			new FieldList(
1716
				// TODO i18n
1717
				new FormAction('submit', _t('Form.SubmitBtnLabel', "Go"))
1718
			)
1719
		);
1720
		$form->addExtraClass('cms-batch-actions form--no-dividers');
1721
		$form->unsetValidator();
1722
1723
		$this->extend('updateBatchActionsForm', $form);
1724
		return $form;
1725
	}
1726
1727
	public function printable() {
1728
		$form = $this->getEditForm($this->currentPageID());
1729
		if(!$form) return false;
1730
1731
		$form->transform(new PrintableTransformation());
1732
		$form->setActions(null);
0 ignored issues
show
Documentation introduced by
null is of type null, but the function expects a object<SilverStripe\Forms\FieldList>.

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

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