Completed
Push — fix-for-add-folder ( c1d355...a17c5c )
by Sam
07:30
created

LeftAndMain::savetreenode()   D

Complexity

Conditions 20
Paths 20

Size

Total Lines 94
Code Lines 58

Duplication

Lines 0
Ratio 0 %
Metric Value
dl 0
loc 94
rs 4.7294
cc 20
eloc 58
nc 20
nop 1

How to fix   Long Method    Complexity   

Long Method

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

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

Commonly applied refactorings include:

1
<?php
2
3
/**
4
 * @package framework
5
 * @subpackage admin
6
 */
7
8
use SilverStripe\Forms\Schema\FormSchema;
9
use SilverStripe\Model\FieldType\DBField;
10
11
/**
12
 * LeftAndMain is the parent class of all the two-pane views in the CMS.
13
 * If you are wanting to add more areas to the CMS, you can do it by subclassing LeftAndMain.
14
 *
15
 * This is essentially an abstract class which should be subclassed.
16
 * See {@link CMSMain} for a good example.
17
 *
18
 * @property FormSchema $schema
19
 */
20
class LeftAndMain extends Controller implements PermissionProvider {
21
22
	/**
23
	 * The 'base' url for CMS administration areas.
24
	 * Note that if this is changed, many javascript
25
	 * behaviours need to be updated with the correct url
26
	 *
27
	 * @config
28
	 * @var string $url_base
29
	 */
30
	private static $url_base = "admin";
31
32
	/**
33
	 * The current url segment attached to the LeftAndMain instance
34
	 *
35
	 * @config
36
	 * @var string
37
	 */
38
	private static $url_segment;
39
40
	/**
41
	 * @config
42
	 * @var string
43
	 */
44
	private static $url_rule = '/$Action/$ID/$OtherID';
45
46
	/**
47
	 * @config
48
	 * @var string
49
	 */
50
	private static $menu_title;
51
52
	/**
53
	 * @config
54
	 * @var string
55
	 */
56
	private static $menu_icon;
57
58
	/**
59
	 * @config
60
	 * @var int
61
	 */
62
	private static $menu_priority = 0;
63
64
	/**
65
	 * @config
66
	 * @var int
67
	 */
68
	private static $url_priority = 50;
69
70
	/**
71
	 * A subclass of {@link DataObject}.
72
	 *
73
	 * Determines what is managed in this interface, through
74
	 * {@link getEditForm()} and other logic.
75
	 *
76
	 * @config
77
	 * @var string
78
	 */
79
	private static $tree_class = null;
80
81
	/**
82
	 * The url used for the link in the Help tab in the backend
83
	 *
84
	 * @config
85
	 * @var string
86
	 */
87
	private static $help_link = '//userhelp.silverstripe.org/framework/en/3.2';
88
89
	/**
90
	 * @var array
91
	 */
92
	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...
93
		'index',
94
		'save',
95
		'savetreenode',
96
		'getsubtree',
97
		'updatetreenodes',
98
		'printable',
99
		'show',
100
		'EditorToolbar',
101
		'EditForm',
102
		'AddForm',
103
		'batchactions',
104
		'BatchActionsForm',
105
		'schema',
106
	];
107
108
	private static $dependencies = [
109
		'schema' => '%$FormSchema'
110
	];
111
112
	/**
113
	 * @config
114
	 * @var Array Codes which are required from the current user to view this controller.
115
	 * If multiple codes are provided, all of them are required.
116
	 * All CMS controllers require "CMS_ACCESS_LeftAndMain" as a baseline check,
117
	 * and fall back to "CMS_ACCESS_<class>" if no permissions are defined here.
118
	 * See {@link canView()} for more details on permission checks.
119
	 */
120
	private static $required_permission_codes;
121
122
	/**
123
	 * @config
124
	 * @var String Namespace for session info, e.g. current record.
125
	 * Defaults to the current class name, but can be amended to share a namespace in case
126
	 * controllers are logically bundled together, and mainly separated
127
	 * to achieve more flexible templating.
128
	 */
129
	private static $session_namespace;
130
131
	/**
132
	 * Register additional requirements through the {@link Requirements} class.
133
	 * Used mainly to work around the missing "lazy loading" functionality
134
	 * for getting css/javascript required after an ajax-call (e.g. loading the editform).
135
	 *
136
	 * YAML configuration example:
137
	 * <code>
138
	 * LeftAndMain:
139
	 *   extra_requirements_javascript:
140
	 *     mysite/javascript/myscript.js:
141
	 * </code>
142
	 *
143
	 * @config
144
	 * @var array
145
	 */
146
	private static $extra_requirements_javascript = array();
147
148
	/**
149
	 * YAML configuration example:
150
	 * <code>
151
	 * LeftAndMain:
152
	 *   extra_requirements_css:
153
	 *     mysite/css/mystyle.css:
154
	 *       media: screen
155
	 * </code>
156
	 *
157
	 * @config
158
	 * @var array See {@link extra_requirements_javascript}
159
	 */
160
	private static $extra_requirements_css = array();
161
162
	/**
163
	 * @config
164
	 * @var array See {@link extra_requirements_javascript}
165
	 */
166
	private static $extra_requirements_themedCss = array();
167
168
	/**
169
	 * If true, call a keepalive ping every 5 minutes from the CMS interface,
170
	 * to ensure that the session never dies.
171
	 *
172
	 * @config
173
	 * @var boolean
174
	 */
175
	private static $session_keepalive_ping = true;
176
177
	/**
178
	 * @var PjaxResponseNegotiator
179
	 */
180
	protected $responseNegotiator;
181
182
	/**
183
	 * Gets the combined configuration of all LeafAndMain subclasses required by the client app.
184
	 *
185
	 * @return array
186
	 *
187
	 * WARNING: Experimental API
188
	 */
189
	public function getCombinedClientConfig() {
190
		$combinedClientConfig = ['sections' => []];
191
		$cmsClassNames = CMSMenu::get_cms_classes('LeftAndMain', true, CMSMenu::URL_PRIORITY);
192
193
		foreach ($cmsClassNames as $className) {
194
			$combinedClientConfig['sections'][$className] =  Injector::inst()->get($className)->getClientConfig();
195
		}
196
197
		return Convert::raw2json($combinedClientConfig);
198
	}
199
200
	/**
201
	 * Returns configuration required by the client app.
202
	 *
203
	 * @return array
204
	 *
205
	 * WARNING: Experimental API
206
	 */
207
	public function getClientConfig() {
208
		return [
209
			'route' => Config::inst()->get($this->class, 'url_segment')
210
		];
211
	}
212
213
	/**
214
	 * Gets a JSON schema representing the current edit form.
215
	 *
216
	 * WARNING: Experimental API.
217
	 *
218
	 * @return SS_HTTPResponse
219
	 */
220
	public function schema($request) {
221
		$response = $this->getResponse();
222
		$formName = $request->param('ID');
223
224
		if(!$this->hasMethod("get{$formName}")) {
225
			throw new SS_HTTPResponse_Exception(
226
				'Form not found',
227
				400
228
			);
229
		}
230
231
		if(!$this->hasAction($formName)) {
232
			throw new SS_HTTPResponse_Exception(
233
				'Form not accessible',
234
				401
235
			);
236
		}
237
238
		$form = $this->{"get{$formName}"}();
239
		$response->addHeader('Content-Type', 'application/json');
240
		$response->setBody(Convert::raw2json($this->getSchemaForForm($form)));
241
242
		return $response;
243
	}
244
245
	/**
246
	 * Returns a representation of the provided {@link Form} as structured data,
247
	 * based on the request data.
248
	 *
249
	 * @param Form $form
250
	 * @return array
251
	 */
252
	protected function getSchemaForForm(Form $form) {
253
		$request = $this->getRequest();
254
		$schemaParts = [];
0 ignored issues
show
Unused Code introduced by
$schemaParts 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...
255
		$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...
256
257
		// Valid values for the "X-Formschema-Request" header are "schema" and "state".
258
		// If either of these values are set they will be stored in the $schemaParst array
259
		// and used to construct the response body.
260
		if ($schemaHeader = $request->getHeader('X-Formschema-Request')) {
261
			$schemaParts = array_filter(explode(',', $schemaHeader), function($value) {
262
				$validHeaderValues = ['schema', 'state'];
263
				return in_array(trim($value), $validHeaderValues);
264
			});
265
		} else {
266
			$schemaParts = ['schema'];
267
		}
268
269
		$return = ['id' => $form->getName()];
270
271
		if (in_array('schema', $schemaParts)) {
272
			$return['schema'] = $this->schema->getSchema($form);
273
		}
274
275
		if (in_array('state', $schemaParts)) {
276
			$return['state'] = $this->schema->getState($form);
277
		}
278
279
		return $return;
280
	}
281
282
	/**
283
	 * @param Member $member
284
	 * @return boolean
285
	 */
286
	public function canView($member = null) {
287
		if(!$member && $member !== FALSE) $member = Member::currentUser();
288
289
		// cms menus only for logged-in members
290
		if(!$member) return false;
291
292
		// alternative extended checks
293
		if($this->hasMethod('alternateAccessCheck')) {
294
			$alternateAllowed = $this->alternateAccessCheck();
295
			if($alternateAllowed === FALSE) return false;
296
		}
297
298
		// Check for "CMS admin" permission
299
		if(Permission::checkMember($member, "CMS_ACCESS_LeftAndMain")) return true;
300
301
		// Check for LeftAndMain sub-class permissions
302
		$codes = array();
303
		$extraCodes = $this->stat('required_permission_codes');
304
		if($extraCodes !== false) { // allow explicit FALSE to disable subclass check
305
			if($extraCodes) $codes = array_merge($codes, (array)$extraCodes);
306
			else $codes[] = "CMS_ACCESS_$this->class";
307
		}
308
		foreach($codes as $code) if(!Permission::checkMember($member, $code)) return false;
309
310
		return true;
311
	}
312
313
	/**
314
	 * @uses LeftAndMainExtension->init()
315
	 * @uses LeftAndMainExtension->accessedCMS()
316
	 * @uses CMSMenu
317
	 */
318
	public function init() {
319
		parent::init();
320
321
		Config::inst()->update('SSViewer', 'rewrite_hash_links', false);
322
		Config::inst()->update('ContentNegotiator', 'enabled', false);
323
324
		// set language
325
		$member = Member::currentUser();
326
		if(!empty($member->Locale)) i18n::set_locale($member->Locale);
327
		if(!empty($member->DateFormat)) i18n::config()->date_format = $member->DateFormat;
0 ignored issues
show
Documentation introduced by
The property date_format does not exist on object<Config_ForClass>. Since you implemented __set, maybe consider adding a @property annotation.

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

<?php

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

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

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

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

}

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

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

See also the PhpDoc documentation for @property.

Loading history...
Documentation introduced by
The property DateFormat does not exist on object<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...
328
		if(!empty($member->TimeFormat)) i18n::config()->time_format = $member->TimeFormat;
0 ignored issues
show
Documentation introduced by
The property time_format does not exist on object<Config_ForClass>. Since you implemented __set, maybe consider adding a @property annotation.

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

<?php

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

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

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

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

}

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

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

See also the PhpDoc documentation for @property.

Loading history...
Documentation introduced by
The property TimeFormat does not exist on object<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...
329
330
		// can't be done in cms/_config.php as locale is not set yet
331
		CMSMenu::add_link(
332
			'Help',
333
			_t('LeftAndMain.HELP', 'Help', 'Menu title'),
334
			$this->config()->help_link,
335
			-2,
336
			array(
337
				'target' => '_blank'
338
			)
339
		);
340
341
		// Allow customisation of the access check by a extension
342
		// Also all the canView() check to execute Controller::redirect()
343
		if(!$this->canView() && !$this->getResponse()->isFinished()) {
344
			// When access /admin/, we should try a redirect to another part of the admin rather than be locked out
345
			$menu = $this->MainMenu();
346
			foreach($menu as $candidate) {
347
				if(
348
					$candidate->Link &&
349
					$candidate->Link != $this->Link()
350
					&& $candidate->MenuItem->controller
351
					&& singleton($candidate->MenuItem->controller)->canView()
352
				) {
353
					return $this->redirect($candidate->Link);
354
				}
355
			}
356
357
			if(Member::currentUser()) {
358
				Session::set("BackURL", null);
359
			}
360
361
			// if no alternate menu items have matched, return a permission error
362
			$messageSet = array(
363
				'default' => _t(
364
					'LeftAndMain.PERMDEFAULT',
365
					"You must be logged in to access the administration area; please enter your credentials below."
366
				),
367
				'alreadyLoggedIn' => _t(
368
					'LeftAndMain.PERMALREADY',
369
					"I'm sorry, but you can't access that part of the CMS.  If you want to log in as someone else, do"
370
					. " so below."
371
				),
372
				'logInAgain' => _t(
373
					'LeftAndMain.PERMAGAIN',
374
					"You have been logged out of the CMS.  If you would like to log in again, enter a username and"
375
					. " password below."
376
				),
377
			);
378
379
			return Security::permissionFailure($this, $messageSet);
380
		}
381
382
		// Don't continue if there's already been a redirection request.
383
		if($this->redirectedTo()) return;
384
385
		// Audit logging hook
386
		if(empty($_REQUEST['executeForm']) && !$this->getRequest()->isAjax()) $this->extend('accessedCMS');
387
388
		// Set the members html editor config
389
		if(Member::currentUser()) {
390
			HtmlEditorConfig::set_active_identifier(Member::currentUser()->getHtmlEditorConfigForCMS());
0 ignored issues
show
Bug introduced by
The method getHtmlEditorConfigForCMS() does not exist on DataObject. Did you maybe mean config()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
391
		}
392
393
		// Set default values in the config if missing.  These things can't be defined in the config
394
		// file because insufficient information exists when that is being processed
395
		$htmlEditorConfig = HtmlEditorConfig::get_active();
396
		$htmlEditorConfig->setOption('language', i18n::get_tinymce_lang());
397
		if(!$htmlEditorConfig->getOption('content_css')) {
398
			$cssFiles = array();
399
			$cssFiles[] = FRAMEWORK_ADMIN_DIR . '/css/editor.css';
400
401
			// Use theme from the site config
402
			if(class_exists('SiteConfig') && ($config = SiteConfig::current_site_config()) && $config->Theme) {
403
				$theme = $config->Theme;
404
			} elseif(Config::inst()->get('SSViewer', 'theme_enabled') && Config::inst()->get('SSViewer', 'theme')) {
405
				$theme = Config::inst()->get('SSViewer', 'theme');
406
			} else {
407
				$theme = false;
408
			}
409
410
			if($theme) $cssFiles[] = THEMES_DIR . "/{$theme}/css/editor.css";
411
			else if(project()) $cssFiles[] = project() . '/css/editor.css';
412
413
			// Remove files that don't exist
414
			foreach($cssFiles as $k => $cssFile) {
415
				if(!file_exists(BASE_PATH . '/' . $cssFile)) unset($cssFiles[$k]);
416
			}
417
418
			$htmlEditorConfig->setOption('content_css', implode(',', $cssFiles));
419
		}
420
421
		Requirements::customScript("
422
			window.ss = window.ss || {};
423
			window.ss.config = " . $this->getCombinedClientConfig() . ";
424
		");
425
426
		Requirements::javascript(FRAMEWORK_ADMIN_DIR . '/javascript/dist/bundle-lib.js', [
427
			'provides' => [
428
				THIRDPARTY_DIR . '/jquery/jquery.js',
429
				THIRDPARTY_DIR . '/jquery-ui/jquery-ui.js',
430
				THIRDPARTY_DIR . '/jquery-entwine/dist/jquery.entwine-dist.js',
431
				THIRDPARTY_DIR . '/jquery-cookie/jquery.cookie.js',
432
				THIRDPARTY_DIR . '/jquery-query/jquery.query.js',
433
				THIRDPARTY_DIR . '/jquery-form/jquery.form.js',
434
				THIRDPARTY_DIR . '/jquery-ondemand/jquery.ondemand.js',
435
				THIRDPARTY_DIR . '/jquery-changetracker/lib/jquery.changetracker.js',
436
				THIRDPARTY_DIR . '/jstree/jquery.jstree.js',
437
				FRAMEWORK_ADMIN_DIR . '/thirdparty/jquery-notice/jquery.notice.js',
438
				FRAMEWORK_ADMIN_DIR . '/thirdparty/jsizes/lib/jquery.sizes.js',
439
				FRAMEWORK_ADMIN_DIR . '/thirdparty/jlayout/lib/jlayout.border.js',
440
				FRAMEWORK_ADMIN_DIR . '/thirdparty/jlayout/lib/jquery.jlayout.js',
441
				FRAMEWORK_ADMIN_DIR . '/thirdparty/chosen/chosen/chosen.jquery.js',
442
				FRAMEWORK_ADMIN_DIR . '/thirdparty/jquery-hoverIntent/jquery.hoverIntent.js',
443
				FRAMEWORK_DIR . '/javascript/dist/TreeDropdownField.js',
444
				FRAMEWORK_DIR . '/javascript/dist/DateField.js',
445
				FRAMEWORK_DIR . '/javascript/dist/HtmlEditorField.js',
446
				FRAMEWORK_DIR . '/javascript/dist/TabSet.js',
447
				FRAMEWORK_DIR . '/javascript/dist/GridField.js',
448
				FRAMEWORK_DIR . '/javascript/dist/i18n.js',
449
				FRAMEWORK_ADMIN_DIR . '/javascript/dist/sspath.js',
450
				FRAMEWORK_ADMIN_DIR . '/javascript/dist/ssui.core.js',
451
			]
452
		]);
453
454
		Requirements::javascript(FRAMEWORK_ADMIN_DIR . '/javascript/dist/bundle-legacy.js', [
455
			'provides' => [
456
				FRAMEWORK_ADMIN_DIR . '/javascript/dist/LeftAndMain.Layout.js',
457
				FRAMEWORK_ADMIN_DIR . '/javascript/dist/LeftAndMain.js',
458
				FRAMEWORK_ADMIN_DIR . '/javascript/dist/LeftAndMain.ActionTabSet.js',
459
				FRAMEWORK_ADMIN_DIR . '/javascript/dist/LeftAndMain.Panel.js',
460
				FRAMEWORK_ADMIN_DIR . '/javascript/dist/LeftAndMain.Tree.js',
461
				FRAMEWORK_ADMIN_DIR . '/javascript/dist/LeftAndMain.Content.js',
462
				FRAMEWORK_ADMIN_DIR . '/javascript/dist/LeftAndMain.EditForm.js',
463
				FRAMEWORK_ADMIN_DIR . '/javascript/dist/LeftAndMain.Menu.js',
464
				FRAMEWORK_ADMIN_DIR . '/javascript/dist/LeftAndMain.Preview.js',
465
				FRAMEWORK_ADMIN_DIR . '/javascript/dist/LeftAndMain.BatchActions.js',
466
				FRAMEWORK_ADMIN_DIR . '/javascript/dist/LeftAndMain.FieldHelp.js',
467
				FRAMEWORK_ADMIN_DIR . '/javascript/dist/LeftAndMain.FieldDescriptionToggle.js',
468
				FRAMEWORK_ADMIN_DIR . '/javascript/dist/LeftAndMain.TreeDropdownField.js'
469
			]
470
		]);
471
472
		Requirements::add_i18n_javascript(FRAMEWORK_DIR . '/javascript/lang', false, true);
473
		Requirements::add_i18n_javascript(FRAMEWORK_ADMIN_DIR . '/javascript/lang', false, true);
474
475
		if ($this->config()->session_keepalive_ping) {
476
			Requirements::javascript(FRAMEWORK_ADMIN_DIR . '/javascript/dist/LeftAndMain.Ping.js');
477
		}
478
479
		if (Director::isDev()) {
480
			// TODO Confuses jQuery.ondemand through document.write()
481
			Requirements::javascript(THIRDPARTY_DIR . '/jquery-entwine/src/jquery.entwine.inspector.js');
482
			Requirements::javascript(FRAMEWORK_ADMIN_DIR . '/javascript/dist/leaktools.js');
483
		}
484
485
		Requirements::javascript(FRAMEWORK_ADMIN_DIR . '/javascript/dist/bundle-framework.js');
486
		
487
		Requirements::css(FRAMEWORK_ADMIN_DIR . '/css/bootstrap/bootstrap.css');
488
		Requirements::css(FRAMEWORK_ADMIN_DIR . '/thirdparty/jquery-notice/jquery.notice.css');
489
		Requirements::css(THIRDPARTY_DIR . '/jquery-ui-themes/smoothness/jquery-ui.css');
490
		Requirements::css(FRAMEWORK_ADMIN_DIR .'/thirdparty/chosen/chosen/chosen.css');
491
		Requirements::css(THIRDPARTY_DIR . '/jstree/themes/apple/style.css');
492
		Requirements::css(FRAMEWORK_DIR . '/css/TreeDropdownField.css');
493
		Requirements::css(FRAMEWORK_ADMIN_DIR . '/css/screen.css');
494
		Requirements::css(FRAMEWORK_DIR . '/css/GridField.css');
495
496
		// Browser-specific requirements
497
		$ie = isset($_SERVER['HTTP_USER_AGENT']) ? strpos($_SERVER['HTTP_USER_AGENT'], 'MSIE') : false;
498
		if($ie) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $ie of type integer|false is loosely compared to true; this is ambiguous if the integer can be zero. You might want to explicitly use !== null instead.

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

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
499
			$version = substr($_SERVER['HTTP_USER_AGENT'], $ie + 5, 3);
500
501
			if($version == 7) {
502
				Requirements::css(FRAMEWORK_ADMIN_DIR . '/css/ie7.css');
503
			} else if($version == 8) {
504
				Requirements::css(FRAMEWORK_ADMIN_DIR . '/css/ie8.css');
505
			}
506
		}
507
508
		// Custom requirements
509
		$extraJs = $this->stat('extra_requirements_javascript');
510
511
		if($extraJs) {
512
			foreach($extraJs as $file => $config) {
0 ignored issues
show
Bug introduced by
The expression $extraJs of type array|integer|double|string|boolean is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

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

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

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

Loading history...
513
				if(is_numeric($file)) {
514
					$file = $config;
515
				}
516
517
				Requirements::javascript($file);
518
			}
519
		}
520
521
		$extraCss = $this->stat('extra_requirements_css');
522
523 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...
524
			foreach($extraCss as $file => $config) {
0 ignored issues
show
Bug introduced by
The expression $extraCss of type array|integer|double|string|boolean is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

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

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

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

Loading history...
525
				if(is_numeric($file)) {
526
					$file = $config;
527
					$config = array();
528
				}
529
530
				Requirements::css($file, isset($config['media']) ? $config['media'] : null);
531
			}
532
		}
533
534
		$extraThemedCss = $this->stat('extra_requirements_themedCss');
535
536 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...
537
			foreach ($extraThemedCss as $file => $config) {
0 ignored issues
show
Bug introduced by
The expression $extraThemedCss of type array|integer|double|string|boolean is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

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

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

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

Loading history...
538
				if(is_numeric($file)) {
539
					$file = $config;
540
					$config = array();
541
				}
542
543
				Requirements::themedCSS($file, isset($config['media']) ? $config['media'] : null);
544
			}
545
		}
546
547
		$dummy = null;
548
		$this->extend('init', $dummy);
549
550
		// The user's theme shouldn't affect the CMS, if, for example, they have
551
		// replaced TableListField.ss or Form.ss.
552
		Config::inst()->update('SSViewer', 'theme_enabled', false);
553
554
		//set the reading mode for the admin to stage
555
		Versioned::set_stage(Versioned::DRAFT);
556
	}
557
558
	public function handleRequest(SS_HTTPRequest $request, DataModel $model = null) {
559
		try {
560
			$response = parent::handleRequest($request, $model);
0 ignored issues
show
Bug introduced by
It seems like $model defined by parameter $model on line 558 can be null; however, Controller::handleRequest() does not accept null, maybe add an additional type check?

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

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

function notNullable(stdClass $x) { }

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

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

// Safe - Alternative 2: Changing Parameter
function withNonNullableParam(stdClass $x) {
    notNullable($x);
}
Loading history...
561
		} catch(ValidationException $e) {
562
			// Nicer presentation of model-level validation errors
563
			$msgs = _t('LeftAndMain.ValidationError', 'Validation error') . ': '
564
				. $e->getMessage();
565
			$e = new SS_HTTPResponse_Exception($msgs, 403);
566
			$errorResponse = $e->getResponse();
567
			$errorResponse->addHeader('Content-Type', 'text/plain');
568
			$errorResponse->addHeader('X-Status', rawurlencode($msgs));
569
			$e->setResponse($errorResponse);
570
			throw $e;
571
		}
572
573
		$title = $this->Title();
574
		if(!$response->getHeader('X-Controller')) $response->addHeader('X-Controller', $this->class);
575
		if(!$response->getHeader('X-Title')) $response->addHeader('X-Title', urlencode($title));
576
577
		// Prevent clickjacking, see https://developer.mozilla.org/en-US/docs/HTTP/X-Frame-Options
578
		$originalResponse = $this->getResponse();
579
		$originalResponse->addHeader('X-Frame-Options', 'SAMEORIGIN');
580
		$originalResponse->addHeader('Vary', 'X-Requested-With');
581
582
		return $response;
583
	}
584
585
	/**
586
	 * Overloaded redirection logic to trigger a fake redirect on ajax requests.
587
	 * While this violates HTTP principles, its the only way to work around the
588
	 * fact that browsers handle HTTP redirects opaquely, no intervention via JS is possible.
589
	 * In isolation, that's not a problem - but combined with history.pushState()
590
	 * it means we would request the same redirection URL twice if we want to update the URL as well.
591
	 * See LeftAndMain.js for the required jQuery ajaxComplete handlers.
592
	 */
593
	public function redirect($url, $code=302) {
594
		if($this->getRequest()->isAjax()) {
595
			$response = $this->getResponse();
596
			$response->addHeader('X-ControllerURL', $url);
597
			if($this->getRequest()->getHeader('X-Pjax') && !$response->getHeader('X-Pjax')) {
598
				$response->addHeader('X-Pjax', $this->getRequest()->getHeader('X-Pjax'));
599
			}
600
			$newResponse = new LeftAndMain_HTTPResponse(
601
				$response->getBody(),
602
				$response->getStatusCode(),
603
				$response->getStatusDescription()
604
			);
605
			foreach($response->getHeaders() as $k => $v) {
606
				$newResponse->addHeader($k, $v);
607
			}
608
			$newResponse->setIsFinished(true);
609
			$this->setResponse($newResponse);
610
			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 Controller::redirect of type SS_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...
611
		} else {
612
			parent::redirect($url, $code);
613
		}
614
	}
615
616
	public function index($request) {
617
		return $this->getResponseNegotiator()->respond($request);
618
	}
619
620
	/**
621
	 * If this is set to true, the "switchView" context in the
622
	 * template is shown, with links to the staging and publish site.
623
	 *
624
	 * @return boolean
625
	 */
626
	public function ShowSwitchView() {
627
		return false;
628
	}
629
630
631
	//------------------------------------------------------------------------------------------//
632
	// Main controllers
633
634
	/**
635
	 * You should implement a Link() function in your subclass of LeftAndMain,
636
	 * to point to the URL of that particular controller.
637
	 *
638
	 * @return string
639
	 */
640
	public function Link($action = null) {
641
		// Handle missing url_segments
642
		if($this->config()->url_segment) {
643
			$segment = $this->config()->get('url_segment', Config::FIRST_SET);
644
		} else {
645
			$segment = $this->class;
646
		};
647
648
		$link = Controller::join_links(
649
			$this->stat('url_base', true),
0 ignored issues
show
Unused Code introduced by
The call to LeftAndMain::stat() has too many arguments starting with true.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
650
			$segment,
651
			'/', // trailing slash needed if $action is null!
652
			"$action"
653
		);
654
		$this->extend('updateLink', $link);
655
		return $link;
656
	}
657
658
	/**
659
	 * Returns the menu title for the given LeftAndMain subclass.
660
	 * Implemented static so that we can get this value without instantiating an object.
661
	 * Menu title is *not* internationalised.
662
	 */
663
	public static function menu_title_for_class($class) {
664
		$title = Config::inst()->get($class, 'menu_title', Config::FIRST_SET);
665
		if(!$title) $title = preg_replace('/Admin$/', '', $class);
666
		return $title;
667
	}
668
669
	/**
670
	 * Return styling for the menu icon, if a custom icon is set for this class
671
	 *
672
	 * Example: static $menu-icon = '/path/to/image/';
673
	 * @param string $class
674
	 * @return string
675
	 */
676
	public static function menu_icon_for_class($class) {
677
		$icon = Config::inst()->get($class, 'menu_icon', Config::FIRST_SET);
678
		if (!empty($icon)) {
679
			$class = strtolower(Convert::raw2htmlname(str_replace('\\', '-', $class)));
680
			return ".icon.icon-16.icon-{$class} { background-image: url('{$icon}'); } ";
681
		}
682
		return '';
683
	}
684
685
	public function show($request) {
686
		// TODO Necessary for TableListField URLs to work properly
687
		if($request->param('ID')) $this->setCurrentPageID($request->param('ID'));
688
		return $this->getResponseNegotiator()->respond($request);
689
	}
690
691
	/**
692
	 * Caution: Volatile API.
693
	 *
694
	 * @return PjaxResponseNegotiator
695
	 */
696
	public function getResponseNegotiator() {
697
		if(!$this->responseNegotiator) {
698
			$controller = $this;
699
			$this->responseNegotiator = new PjaxResponseNegotiator(
700
				array(
701
					'CurrentForm' => function() use(&$controller) {
702
						return $controller->getEditForm()->forTemplate();
703
					},
704
					'Content' => function() use(&$controller) {
705
						return $controller->renderWith($controller->getTemplatesWithSuffix('_Content'));
706
					},
707
					'Breadcrumbs' => function() use (&$controller) {
708
						return $controller->renderWith('CMSBreadcrumbs');
709
					},
710
					'default' => function() use(&$controller) {
711
						return $controller->renderWith($controller->getViewer('show'));
712
					}
713
				),
714
				$this->getResponse()
715
			);
716
		}
717
		return $this->responseNegotiator;
718
	}
719
720
	//------------------------------------------------------------------------------------------//
721
	// Main UI components
722
723
	/**
724
	 * Returns the main menu of the CMS.  This is also used by init()
725
	 * to work out which sections the user has access to.
726
	 *
727
	 * @param Boolean
728
	 * @return SS_List
729
	 */
730
	public function MainMenu($cached = true) {
731
		if(!isset($this->_cache_MainMenu) || !$cached) {
732
			// Don't accidentally return a menu if you're not logged in - it's used to determine access.
733
			if(!Member::currentUser()) return new ArrayList();
734
735
			// Encode into DO set
736
			$menu = new ArrayList();
737
			$menuItems = CMSMenu::get_viewable_menu_items();
738
739
			// extra styling for custom menu-icons
740
			$menuIconStyling = '';
741
742
			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...
743
				foreach($menuItems as $code => $menuItem) {
744
					// alternate permission checks (in addition to LeftAndMain->canView())
745
746
					if(
747
						isset($menuItem->controller)
748
						&& $this->hasMethod('alternateMenuDisplayCheck')
749
						&& !$this->alternateMenuDisplayCheck($menuItem->controller)
750
					) {
751
						continue;
752
					}
753
754
					$linkingmode = "link";
755
756
					if($menuItem->controller && get_class($this) == $menuItem->controller) {
757
						$linkingmode = "current";
758
					} else if(strpos($this->Link(), $menuItem->url) !== false) {
759
						if($this->Link() == $menuItem->url) {
760
							$linkingmode = "current";
761
762
						// default menu is the one with a blank {@link url_segment}
763
						} else if(singleton($menuItem->controller)->stat('url_segment') == '') {
764
							if($this->Link() == $this->stat('url_base').'/') {
765
								$linkingmode = "current";
766
							}
767
768
						} else {
769
							$linkingmode = "current";
770
						}
771
					}
772
773
					// already set in CMSMenu::populate_menu(), but from a static pre-controller
774
					// context, so doesn't respect the current user locale in _t() calls - as a workaround,
775
					// we simply call LeftAndMain::menu_title_for_class() again
776
					// if we're dealing with a controller
777
					if($menuItem->controller) {
778
						$defaultTitle = LeftAndMain::menu_title_for_class($menuItem->controller);
779
						$title = _t("{$menuItem->controller}.MENUTITLE", $defaultTitle);
780
					} else {
781
						$title = $menuItem->title;
782
					}
783
784
					// Provide styling for custom $menu-icon. Done here instead of in
785
					// CMSMenu::populate_menu(), because the icon is part of
786
					// the CMS right pane for the specified class as well...
787
					if($menuItem->controller) {
788
						$menuIcon = LeftAndMain::menu_icon_for_class($menuItem->controller);
789
						if (!empty($menuIcon)) $menuIconStyling .= $menuIcon;
790
					}
791
792
					$menu->push(new ArrayData(array(
793
						"MenuItem" => $menuItem,
794
						"AttributesHTML" => $menuItem->getAttributesHTML(),
795
						"Title" => Convert::raw2xml($title),
796
						"Code" => DBField::create_field('Text', $code),
797
						"Link" => $menuItem->url,
798
						"LinkingMode" => $linkingmode
799
					)));
800
				}
801
			}
802
			if ($menuIconStyling) Requirements::customCSS($menuIconStyling);
803
804
			$this->_cache_MainMenu = $menu;
0 ignored issues
show
Documentation introduced by
The property _cache_MainMenu does not exist on object<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...
805
		}
806
807
		return $this->_cache_MainMenu;
808
	}
809
810
	public function Menu() {
811
		return $this->renderWith($this->getTemplatesWithSuffix('_Menu'));
812
	}
813
814
	/**
815
	 * @todo Wrap in CMSMenu instance accessor
816
	 * @return ArrayData A single menu entry (see {@link MainMenu})
817
	 */
818
	public function MenuCurrentItem() {
819
		$items = $this->MainMenu();
820
		return $items->find('LinkingMode', 'current');
821
	}
822
823
	/**
824
	 * Return a list of appropriate templates for this class, with the given suffix using
825
	 * {@link SSViewer::get_templates_by_class()}
826
	 *
827
	 * @return array
828
	 */
829
	public function getTemplatesWithSuffix($suffix) {
830
		return SSViewer::get_templates_by_class(get_class($this), $suffix, 'LeftAndMain');
831
	}
832
833
	public function Content() {
834
		return $this->renderWith($this->getTemplatesWithSuffix('_Content'));
835
	}
836
837
	public function getRecord($id) {
838
		$className = $this->stat('tree_class');
839
		if($className && $id instanceof $className) {
840
			return $id;
841
		} else if($className && $id == 'root') {
842
			return singleton($className);
843
		} else if($className && is_numeric($id)) {
844
			return DataObject::get_by_id($className, $id);
845
		} else {
846
			return false;
847
		}
848
	}
849
850
	/**
851
	 * @return ArrayList
852
	 */
853
	public function Breadcrumbs($unlinked = false) {
854
		$defaultTitle = LeftAndMain::menu_title_for_class($this->class);
855
		$title = _t("{$this->class}.MENUTITLE", $defaultTitle);
856
		$items = new ArrayList(array(
857
			new ArrayData(array(
858
				'Title' => $title,
859
				'Link' => ($unlinked) ? false : $this->Link()
860
			))
861
		));
862
		$record = $this->currentPage();
863
		if($record && $record->exists()) {
864
			if($record->hasExtension('Hierarchy')) {
865
				$ancestors = $record->getAncestors();
866
				$ancestors = new ArrayList(array_reverse($ancestors->toArray()));
867
				$ancestors->push($record);
868
				foreach($ancestors as $ancestor) {
869
					$items->push(new ArrayData(array(
870
						'Title' => ($ancestor->MenuTitle) ? $ancestor->MenuTitle : $ancestor->Title,
871
						'Link' => ($unlinked) ? false : Controller::join_links($this->Link('show'), $ancestor->ID)
872
					)));
873
				}
874
			} else {
875
				$items->push(new ArrayData(array(
876
					'Title' => ($record->MenuTitle) ? $record->MenuTitle : $record->Title,
877
					'Link' => ($unlinked) ? false : Controller::join_links($this->Link('show'), $record->ID)
878
				)));
879
			}
880
		}
881
882
		return $items;
883
	}
884
885
	/**
886
	 * @return String HTML
887
	 */
888
	public function SiteTreeAsUL() {
889
		$html = $this->getSiteTreeFor($this->stat('tree_class'));
890
		$this->extend('updateSiteTreeAsUL', $html);
891
		return $html;
892
	}
893
894
	/**
895
	 * Gets the current search filter for this request, if available
896
	 *
897
	 * @throws InvalidArgumentException
898
	 * @return LeftAndMain_SearchFilter
899
	 */
900
	protected function getSearchFilter() {
901
		// Check for given FilterClass
902
		$params = $this->getRequest()->getVar('q');
903
		if(empty($params['FilterClass'])) {
904
			return null;
905
		}
906
907
		// Validate classname
908
		$filterClass = $params['FilterClass'];
909
		$filterInfo = new ReflectionClass($filterClass);
910
		if(!$filterInfo->implementsInterface('LeftAndMain_SearchFilter')) {
911
			throw new InvalidArgumentException(sprintf('Invalid filter class passed: %s', $filterClass));
912
		}
913
914
		return Injector::inst()->createWithArgs($filterClass, array($params));
915
	}
916
917
	/**
918
	 * Get a site tree HTML listing which displays the nodes under the given criteria.
919
	 *
920
	 * @param $className The class of the root object
921
	 * @param $rootID The ID of the root object.  If this is null then a complete tree will be
922
	 *  shown
923
	 * @param $childrenMethod The method to call to get the children of the tree. For example,
924
	 *  Children, AllChildrenIncludingDeleted, or AllHistoricalChildren
925
	 * @return String Nested unordered list with links to each page
926
	 */
927
	public function getSiteTreeFor($className, $rootID = null, $childrenMethod = null, $numChildrenMethod = null,
928
			$filterFunction = null, $nodeCountThreshold = 30) {
929
930
		// Filter criteria
931
		$filter = $this->getSearchFilter();
932
933
		// Default childrenMethod and numChildrenMethod
934
		if(!$childrenMethod) $childrenMethod = ($filter && $filter->getChildrenMethod())
935
			? $filter->getChildrenMethod()
936
			: 'AllChildrenIncludingDeleted';
937
938
		if(!$numChildrenMethod) {
939
			$numChildrenMethod = 'numChildren';
940
			if($filter && $filter->getNumChildrenMethod()) {
941
				$numChildrenMethod = $filter->getNumChildrenMethod();
942
			}
943
		}
944
		if(!$filterFunction && $filter) {
945
			$filterFunction = function($node) use($filter) {
946
				return $filter->isPageIncluded($node);
947
			};
948
		}
949
950
		// Get the tree root
951
		$record = ($rootID) ? $this->getRecord($rootID) : null;
952
		$obj = $record ? $record : singleton($className);
953
954
		// Get the current page
955
		// NOTE: This *must* be fetched before markPartialTree() is called, as this
956
		// causes the Hierarchy::$marked cache to be flushed (@see CMSMain::getRecord)
957
		// which means that deleted pages stored in the marked tree would be removed
958
		$currentPage = $this->currentPage();
959
960
		// Mark the nodes of the tree to return
961
		if ($filterFunction) $obj->setMarkingFilterFunction($filterFunction);
962
963
		$obj->markPartialTree($nodeCountThreshold, $this, $childrenMethod, $numChildrenMethod);
964
965
		// Ensure current page is exposed
966
		if($currentPage) $obj->markToExpose($currentPage);
967
968
		// NOTE: SiteTree/CMSMain coupling :-(
969
		if(class_exists('SiteTree')) {
970
			SiteTree::prepopulate_permission_cache('CanEditType', $obj->markedNodeIDs(),
971
				'SiteTree::can_edit_multiple');
972
		}
973
974
		// getChildrenAsUL is a flexible and complex way of traversing the tree
975
		$controller = $this;
976
		$recordController = ($this->stat('tree_class') == 'SiteTree') ?  singleton('CMSPageEditController') : $this;
977
		$titleFn = function(&$child, $numChildrenMethod) use(&$controller, &$recordController, $filter) {
978
			$link = Controller::join_links($recordController->Link("show"), $child->ID);
979
			$node = LeftAndMain_TreeNode::create($child, $link, $controller->isCurrentPage($child), $numChildrenMethod, $filter);
980
			return $node->forTemplate();
981
		};
982
983
		// Limit the amount of nodes shown for performance reasons.
984
		// Skip the check if we're filtering the tree, since its not clear how many children will
985
		// match the filter criteria until they're queried (and matched up with previously marked nodes).
986
		$nodeThresholdLeaf = Config::inst()->get('Hierarchy', 'node_threshold_leaf');
987
		if($nodeThresholdLeaf && !$filterFunction) {
988
			$nodeCountCallback = function($parent, $numChildren) use(&$controller, $className, $nodeThresholdLeaf) {
989
				if($className == 'SiteTree' && $parent->ID && $numChildren > $nodeThresholdLeaf) {
990
					return sprintf(
991
						'<ul><li class="readonly"><span class="item">'
992
							. '%s (<a href="%s" class="cms-panel-link" data-pjax-target="Content">%s</a>)'
993
							. '</span></li></ul>',
994
						_t('LeftAndMain.TooManyPages', 'Too many pages'),
995
						Controller::join_links(
996
							$controller->LinkWithSearch($controller->Link()), '
997
							?view=list&ParentID=' . $parent->ID
998
						),
999
						_t(
1000
							'LeftAndMain.ShowAsList',
1001
							'show as list',
1002
							'Show large amount of pages in list instead of tree view'
1003
						)
1004
					);
1005
				}
1006
			};
1007
		} else {
1008
			$nodeCountCallback = null;
1009
		}
1010
1011
		// If the amount of pages exceeds the node thresholds set, use the callback
1012
		$html = null;
1013
		if($obj->ParentID && $nodeCountCallback) {
1014
			$html = $nodeCountCallback($obj, $obj->$numChildrenMethod());
1015
		}
1016
1017
		// Otherwise return the actual tree (which might still filter leaf thresholds on children)
1018
		if(!$html) {
1019
			$html = $obj->getChildrenAsUL(
1020
				"",
1021
				$titleFn,
1022
				singleton('CMSPagesController'),
1023
				true,
1024
				$childrenMethod,
1025
				$numChildrenMethod,
1026
				$nodeCountThreshold,
1027
				$nodeCountCallback
1028
			);
1029
		}
1030
1031
		// Wrap the root if needs be.
1032
		if(!$rootID) {
1033
			$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...
1034
1035
			// This lets us override the tree title with an extension
1036
			if($this->hasMethod('getCMSTreeTitle') && $customTreeTitle = $this->getCMSTreeTitle()) {
1037
				$treeTitle = $customTreeTitle;
1038
			} elseif(class_exists('SiteConfig')) {
1039
				$siteConfig = SiteConfig::current_site_config();
1040
				$treeTitle =  Convert::raw2xml($siteConfig->Title);
1041
			} else {
1042
				$treeTitle = '...';
1043
			}
1044
1045
			$html = "<ul><li id=\"record-0\" data-id=\"0\" class=\"Root nodelete\"><strong>$treeTitle</strong>"
1046
				. $html . "</li></ul>";
1047
		}
1048
1049
		return $html;
1050
	}
1051
1052
	/**
1053
	 * Get a subtree underneath the request param 'ID'.
1054
	 * If ID = 0, then get the whole tree.
1055
	 */
1056
	public function getsubtree($request) {
1057
		$html = $this->getSiteTreeFor(
1058
			$this->stat('tree_class'),
1059
			$request->getVar('ID'),
1060
			null,
1061
			null,
1062
			null,
1063
			$request->getVar('minNodeCount')
1064
		);
1065
1066
		// Trim off the outer tag
1067
		$html = preg_replace('/^[\s\t\r\n]*<ul[^>]*>/','', $html);
1068
		$html = preg_replace('/<\/ul[^>]*>[\s\t\r\n]*$/','', $html);
1069
1070
		return $html;
1071
	}
1072
1073
	/**
1074
	 * Allows requesting a view update on specific tree nodes.
1075
	 * Similar to {@link getsubtree()}, but doesn't enforce loading
1076
	 * all children with the node. Useful to refresh views after
1077
	 * state modifications, e.g. saving a form.
1078
	 *
1079
	 * @return String JSON
1080
	 */
1081
	public function updatetreenodes($request) {
1082
		$data = array();
1083
		$ids = explode(',', $request->getVar('ids'));
1084
		foreach($ids as $id) {
1085
			if($id === "") continue; // $id may be a blank string, which is invalid and should be skipped over
1086
1087
			$record = $this->getRecord($id);
1088
			if(!$record) continue; // In case a page is no longer available
1089
			$recordController = ($this->stat('tree_class') == 'SiteTree')
1090
				?  singleton('CMSPageEditController')
1091
				: $this;
1092
1093
			// Find the next & previous nodes, for proper positioning (Sort isn't good enough - it's not a raw offset)
1094
			// TODO: These methods should really be in hierarchy - for a start it assumes Sort exists
1095
			$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...
1096
1097
			$className = $this->stat('tree_class');
1098
			$next = DataObject::get($className)
1099
				->filter('ParentID', $record->ParentID)
1100
				->filter('Sort:GreaterThan', $record->Sort)
1101
				->first();
1102
1103
			if (!$next) {
1104
				$prev = DataObject::get($className)
1105
					->filter('ParentID', $record->ParentID)
1106
					->filter('Sort:LessThan', $record->Sort)
1107
					->reverse()
1108
					->first();
1109
			}
1110
1111
			$link = Controller::join_links($recordController->Link("show"), $record->ID);
1112
			$html = LeftAndMain_TreeNode::create($record, $link, $this->isCurrentPage($record))
1113
				->forTemplate() . '</li>';
1114
1115
			$data[$id] = array(
1116
				'html' => $html,
1117
				'ParentID' => $record->ParentID,
1118
				'NextID' => $next ? $next->ID : null,
1119
				'PrevID' => $prev ? $prev->ID : null
1120
			);
1121
		}
1122
		$this->getResponse()->addHeader('Content-Type', 'text/json');
1123
		return Convert::raw2json($data);
1124
	}
1125
1126
	/**
1127
	 * Save  handler
1128
	 */
1129
	public function save($data, $form) {
1130
		$request = $this->getRequest();
1131
		$className = $this->stat('tree_class');
1132
1133
		// Existing or new record?
1134
		$id = $data['ID'];
1135
		if(substr($id,0,3) != 'new') {
1136
			$record = DataObject::get_by_id($className, $id);
1137
			if($record && !$record->canEdit()) return Security::permissionFailure($this);
1138
			if(!$record || !$record->ID) $this->httpError(404, "Bad record ID #" . (int)$id);
1139
		} else {
1140
			if(!singleton($this->stat('tree_class'))->canCreate()) return Security::permissionFailure($this);
1141
			$record = $this->getNewItem($id, false);
1142
		}
1143
1144
		// save form data into record
1145
		$form->saveInto($record, true);
1146
		$record->write();
1147
		$this->extend('onAfterSave', $record);
1148
		$this->setCurrentPageID($record->ID);
1149
1150
		$this->getResponse()->addHeader('X-Status', rawurlencode(_t('LeftAndMain.SAVEDUP', 'Saved.')));
1151
1152
		if($request->getHeader('X-Formschema-Request')) {
1153
			$data = $this->getSchemaForForm($form);
1154
			$response = new SS_HTTPResponse(Convert::raw2json($data));
1155
			$response->addHeader('Content-Type', 'application/json');
1156
		} else {
1157
			$response = $this->getResponseNegotiator()->respond($request);
1158
		}
1159
1160
		return $response;
1161
	}
1162
1163
	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...
1164
		$className = $this->stat('tree_class');
1165
1166
		$id = $data['ID'];
1167
		$record = DataObject::get_by_id($className, $id);
1168
		if($record && !$record->canDelete()) return Security::permissionFailure();
1169
		if(!$record || !$record->ID) $this->httpError(404, "Bad record ID #" . (int)$id);
1170
1171
		$record->delete();
1172
1173
		$this->getResponse()->addHeader('X-Status', rawurlencode(_t('LeftAndMain.DELETED', 'Deleted.')));
1174
		return $this->getResponseNegotiator()->respond(
1175
			$this->getRequest(),
1176
			array('currentform' => array($this, 'EmptyForm'))
1177
		);
1178
	}
1179
1180
	/**
1181
	 * Update the position and parent of a tree node.
1182
	 * Only saves the node if changes were made.
1183
	 *
1184
	 * Required data:
1185
	 * - 'ID': The moved node
1186
	 * - 'ParentID': New parent relation of the moved node (0 for root)
1187
	 * - 'SiblingIDs': Array of all sibling nodes to the moved node (incl. the node itself).
1188
	 *   In case of a 'ParentID' change, relates to the new siblings under the new parent.
1189
	 *
1190
	 * @return SS_HTTPResponse JSON string with a
1191
	 */
1192
	public function savetreenode($request) {
1193
		if (!Permission::check('SITETREE_REORGANISE') && !Permission::check('ADMIN')) {
1194
			$this->getResponse()->setStatusCode(
1195
				403,
1196
				_t('LeftAndMain.CANT_REORGANISE',
1197
					"You do not have permission to rearange the site tree. Your change was not saved.")
1198
			);
1199
			return;
1200
		}
1201
1202
		$className = $this->stat('tree_class');
1203
		$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...
1204
		$id = $request->requestVar('ID');
1205
		$parentID = $request->requestVar('ParentID');
1206
1207
		if($className == 'SiteTree' && $page = DataObject::get_by_id('Page', $id)){
1208
			$root = $page->getParentType();
1209
			if(($parentID == '0' || $root == 'root') && !SiteConfig::current_site_config()->canCreateTopLevel()){
1210
				$this->getResponse()->setStatusCode(
1211
					403,
1212
					_t('LeftAndMain.CANT_REORGANISE',
1213
						"You do not have permission to alter Top level pages. Your change was not saved.")
1214
					);
1215
				return;
1216
			}
1217
		}
1218
1219
		$siblingIDs = $request->requestVar('SiblingIDs');
1220
		$statusUpdates = array('modified'=>array());
1221
		if(!is_numeric($id) || !is_numeric($parentID)) throw new InvalidArgumentException();
1222
1223
		$node = DataObject::get_by_id($className, $id);
1224
		if($node && !$node->canEdit()) return Security::permissionFailure($this);
1225
1226
		if(!$node) {
1227
			$this->getResponse()->setStatusCode(
1228
				500,
1229
				_t('LeftAndMain.PLEASESAVE',
1230
					"Please Save Page: This page could not be updated because it hasn't been saved yet."
1231
				)
1232
			);
1233
			return;
1234
		}
1235
1236
		// Update hierarchy (only if ParentID changed)
1237
		if($node->ParentID != $parentID) {
1238
			$node->ParentID = (int)$parentID;
0 ignored issues
show
Documentation introduced by
The property ParentID does not exist on object<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...
1239
			$node->write();
1240
1241
			$statusUpdates['modified'][$node->ID] = array(
1242
				'TreeTitle'=>$node->TreeTitle
1243
			);
1244
1245
			// Update all dependent pages
1246
			if(class_exists('VirtualPage')) {
1247
				$virtualPages = VirtualPage::get()->filter("CopyContentFromID", $node->ID);
1248
				foreach($virtualPages as $virtualPage) {
1249
					$statusUpdates['modified'][$virtualPage->ID] = array(
1250
						'TreeTitle' => $virtualPage->TreeTitle()
1251
					);
1252
				}
1253
			}
1254
1255
			$this->getResponse()->addHeader('X-Status',
1256
				rawurlencode(_t('LeftAndMain.REORGANISATIONSUCCESSFUL', 'Reorganised the site tree successfully.')));
1257
		}
1258
1259
		// Update sorting
1260
		if(is_array($siblingIDs)) {
1261
			$counter = 0;
1262
			foreach($siblingIDs as $id) {
1263
				if($id == $node->ID) {
1264
					$node->Sort = ++$counter;
0 ignored issues
show
Documentation introduced by
The property Sort does not exist on object<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...
1265
					$node->write();
1266
					$statusUpdates['modified'][$node->ID] = array(
1267
						'TreeTitle' => $node->TreeTitle
1268
					);
1269
				} else if(is_numeric($id)) {
1270
					// Nodes that weren't "actually moved" shouldn't be registered as
1271
					// having been edited; do a direct SQL update instead
1272
					++$counter;
1273
					DB::prepared_query(
1274
						"UPDATE \"$className\" SET \"Sort\" = ? WHERE \"ID\" = ?",
1275
						array($counter, $id)
1276
					);
1277
				}
1278
			}
1279
1280
			$this->getResponse()->addHeader('X-Status',
1281
				rawurlencode(_t('LeftAndMain.REORGANISATIONSUCCESSFUL', 'Reorganised the site tree successfully.')));
1282
		}
1283
1284
		return Convert::raw2json($statusUpdates);
0 ignored issues
show
Bug Best Practice introduced by
The return type of return \Convert::raw2json($statusUpdates); (string) is incompatible with the return type documented by LeftAndMain::savetreenode of type SS_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...
1285
	}
1286
1287
	public function CanOrganiseSitetree() {
1288
		return !Permission::check('SITETREE_REORGANISE') && !Permission::check('ADMIN') ? false : true;
1289
	}
1290
1291
	/**
1292
	 * Retrieves an edit form, either for display, or to process submitted data.
1293
	 * Also used in the template rendered through {@link Right()} in the $EditForm placeholder.
1294
	 *
1295
	 * This is a "pseudo-abstract" methoed, usually connected to a {@link getEditForm()}
1296
	 * method in an entwine subclass. This method can accept a record identifier,
1297
	 * selected either in custom logic, or through {@link currentPageID()}.
1298
	 * The form usually construct itself from {@link DataObject->getCMSFields()}
1299
	 * for the specific managed subclass defined in {@link LeftAndMain::$tree_class}.
1300
	 *
1301
	 * @param HTTPRequest $request Optionally contains an identifier for the
1302
	 *  record to load into the form.
1303
	 * @return Form Should return a form regardless wether a record has been found.
1304
	 *  Form might be readonly if the current user doesn't have the permission to edit
1305
	 *  the record.
1306
	 */
1307
	/**
1308
	 * @return Form
1309
	 */
1310
	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...
1311
		return $this->getEditForm();
1312
	}
1313
1314
	/**
1315
	 * Calls {@link SiteTree->getCMSFields()}
1316
	 *
1317
	 * @param Int $id
1318
	 * @param FieldList $fields
1319
	 * @return Form
1320
	 */
1321
	public function getEditForm($id = null, $fields = null) {
1322
		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...
1323
1324
		if(is_object($id)) {
1325
			$record = $id;
1326
		} else {
1327
			$record = $this->getRecord($id);
1328
			if($record && !$record->canView()) return Security::permissionFailure($this);
0 ignored issues
show
Bug Compatibility introduced by
The expression \Security::permissionFailure($this); of type SS_HTTPResponse|null adds the type SS_HTTPResponse to the return on line 1328 which is incompatible with the return type documented by LeftAndMain::getEditForm of type Form|null.
Loading history...
1329
		}
1330
1331
		if($record) {
1332
			$fields = ($fields) ? $fields : $record->getCMSFields();
1333
			if ($fields == null) {
1334
				user_error(
1335
					"getCMSFields() returned null  - it should return a FieldList object.
1336
					Perhaps you forgot to put a return statement at the end of your method?",
1337
					E_USER_ERROR
1338
				);
1339
			}
1340
1341
			// Add hidden fields which are required for saving the record
1342
			// and loading the UI state
1343
			if(!$fields->dataFieldByName('ClassName')) {
1344
				$fields->push(new HiddenField('ClassName'));
1345
			}
1346
1347
			$tree_class = $this->stat('tree_class');
1348
			if(
1349
				$tree_class::has_extension('Hierarchy')
1350
				&& !$fields->dataFieldByName('ParentID')
1351
			) {
1352
				$fields->push(new HiddenField('ParentID'));
1353
			}
1354
1355
			// Added in-line to the form, but plucked into different view by frontend scripts.
1356
			if(in_array('CMSPreviewable', class_implements($record))) {
1357
				$navField = new LiteralField('SilverStripeNavigator', $this->getSilverStripeNavigator());
1358
				$navField->setAllowHTML(true);
1359
				$fields->push($navField);
1360
			}
1361
1362
			if($record->hasMethod('getAllCMSActions')) {
1363
				$actions = $record->getAllCMSActions();
1364
			} else {
1365
				$actions = $record->getCMSActions();
1366
				// add default actions if none are defined
1367
				if(!$actions || !$actions->Count()) {
1368 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...
1369
						$actions->push(
1370
							FormAction::create('save',_t('CMSMain.SAVE','Save'))
1371
								->addExtraClass('ss-ui-action-constructive')->setAttribute('data-icon', 'accept')
1372
						);
1373
					}
1374 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...
1375
						$actions->push(
1376
							FormAction::create('delete',_t('ModelAdmin.DELETE','Delete'))
1377
								->addExtraClass('ss-ui-action-destructive')
1378
						);
1379
					}
1380
				}
1381
			}
1382
1383
			// Use <button> to allow full jQuery UI styling
1384
			$actionsFlattened = $actions->dataFields();
1385
			if($actionsFlattened) foreach($actionsFlattened as $action) $action->setUseButtonTag(true);
1386
1387
			$negotiator = $this->getResponseNegotiator();
1388
			$form = Form::create(
1389
				$this, "EditForm", $fields, $actions
1390
			)->setHTMLID('Form_EditForm');
1391
			$form->addExtraClass('cms-edit-form');
1392
			$form->loadDataFrom($record);
1393
			$form->setTemplate($this->getTemplatesWithSuffix('_EditForm'));
0 ignored issues
show
Documentation introduced by
$this->getTemplatesWithSuffix('_EditForm') is of type array, but the function expects a string.

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1394
			$form->setAttribute('data-pjax-fragment', 'CurrentForm');
1395
			$form->setValidationResponseCallback(function() use ($negotiator, $form) {
1396
				$request = $this->getRequest();
1397
				if($request->isAjax() && $negotiator) {
1398
					$form->setupFormErrors();
1399
					$result = $form->forTemplate();
1400
1401
					return $negotiator->respond($request, array(
1402
						'CurrentForm' => function() use($result) {
1403
							return $result;
1404
						}
1405
					));
1406
				}
1407
			});
1408
1409
			// Announce the capability so the frontend can decide whether to allow preview or not.
1410
			if(in_array('CMSPreviewable', class_implements($record))) {
1411
				$form->addExtraClass('cms-previewable');
1412
			}
1413
1414
			// Set this if you want to split up tabs into a separate header row
1415
			// if($form->Fields()->hasTabset()) {
1416
			// 	$form->Fields()->findOrMakeTab('Root')->setTemplate('CMSTabSet');
1417
			// }
1418
1419
			// Add a default or custom validator.
1420
			// @todo Currently the default Validator.js implementation
1421
			//  adds javascript to the document body, meaning it won't
1422
			//  be included properly if the associated fields are loaded
1423
			//  through ajax. This means only serverside validation
1424
			//  will kick in for pages+validation loaded through ajax.
1425
			//  This will be solved by using less obtrusive javascript validation
1426
			//  in the future, see http://open.silverstripe.com/ticket/2915 and
1427
			//  http://open.silverstripe.com/ticket/3386
1428
			if($record->hasMethod('getCMSValidator')) {
1429
				$validator = $record->getCMSValidator();
1430
				// The clientside (mainly LeftAndMain*.js) rely on ajax responses
1431
				// which can be evaluated as javascript, hence we need
1432
				// to override any global changes to the validation handler.
1433
				if($validator != NULL){
1434
					$form->setValidator($validator);
1435
				}
1436
			} else {
1437
				$form->unsetValidator();
1438
			}
1439
1440
			if($record->hasMethod('canEdit') && !$record->canEdit()) {
1441
				$readonlyFields = $form->Fields()->makeReadonly();
1442
				$form->setFields($readonlyFields);
1443
			}
1444
		} else {
1445
			$form = $this->EmptyForm();
1446
		}
1447
1448
		return $form;
1449
	}
1450
1451
	/**
1452
	 * Returns a placeholder form, used by {@link getEditForm()} if no record is selected.
1453
	 * Our javascript logic always requires a form to be present in the CMS interface.
1454
	 *
1455
	 * @return Form
1456
	 */
1457
	public function EmptyForm() {
1458
		$form = Form::create(
1459
			$this,
1460
			"EditForm",
1461
			new FieldList(
1462
				// new HeaderField(
1463
				// 	'WelcomeHeader',
1464
				// 	$this->getApplicationName()
1465
				// ),
1466
				// new LiteralField(
1467
				// 	'WelcomeText',
1468
				// 	sprintf('<p id="WelcomeMessage">%s %s. %s</p>',
1469
				// 		_t('LeftAndMain_right_ss.WELCOMETO','Welcome to'),
1470
				// 		$this->getApplicationName(),
1471
				// 		_t('CHOOSEPAGE','Please choose an item from the left.')
1472
				// 	)
1473
				// )
1474
			),
1475
			new FieldList()
1476
		)->setHTMLID('Form_EditForm');
1477
		$form->unsetValidator();
1478
		$form->addExtraClass('cms-edit-form');
1479
		$form->addExtraClass('root-form');
1480
		$form->setTemplate($this->getTemplatesWithSuffix('_EditForm'));
0 ignored issues
show
Documentation introduced by
$this->getTemplatesWithSuffix('_EditForm') is of type array, but the function expects a string.

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1481
		$form->setAttribute('data-pjax-fragment', 'CurrentForm');
1482
1483
		return $form;
1484
	}
1485
1486
	/**
1487
	 * Return the CMS's HTML-editor toolbar
1488
	 */
1489
	public function EditorToolbar() {
1490
		return HtmlEditorField_Toolbar::create($this, "EditorToolbar");
1491
	}
1492
1493
	/**
1494
	 * Renders a panel containing tools which apply to all displayed
1495
	 * "content" (mostly through {@link EditForm()}), for example a tree navigation or a filter panel.
1496
	 * Auto-detects applicable templates by naming convention: "<controller classname>_Tools.ss",
1497
	 * and takes the most specific template (see {@link getTemplatesWithSuffix()}).
1498
	 * To explicitly disable the panel in the subclass, simply create a more specific, empty template.
1499
	 *
1500
	 * @return String HTML
1501
	 */
1502 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...
1503
		$templates = $this->getTemplatesWithSuffix('_Tools');
1504
		if($templates) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $templates of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
1505
			$viewer = new SSViewer($templates);
1506
			return $viewer->process($this);
1507
		} else {
1508
			return false;
1509
		}
1510
	}
1511
1512
	/**
1513
	 * Renders a panel containing tools which apply to the currently displayed edit form.
1514
	 * The main difference to {@link Tools()} is that the panel is displayed within
1515
	 * the element structure of the form panel (rendered through {@link EditForm}).
1516
	 * This means the panel will be loaded alongside new forms, and refreshed upon save,
1517
	 * which can mean a performance hit, depending on how complex your panel logic gets.
1518
	 * Any form fields contained in the returned markup will also be submitted with the main form,
1519
	 * which might be desired depending on the implementation details.
1520
	 *
1521
	 * @return String HTML
1522
	 */
1523 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...
1524
		$templates = $this->getTemplatesWithSuffix('_EditFormTools');
1525
		if($templates) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $templates of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
1526
			$viewer = new SSViewer($templates);
1527
			return $viewer->process($this);
1528
		} else {
1529
			return false;
1530
		}
1531
	}
1532
1533
	/**
1534
	 * Batch Actions Handler
1535
	 */
1536
	public function batchactions() {
1537
		return new CMSBatchActionHandler($this, 'batchactions', $this->stat('tree_class'));
1538
	}
1539
1540
	/**
1541
	 * @return Form
1542
	 */
1543
	public function BatchActionsForm() {
1544
		$actions = $this->batchactions()->batchActionList();
1545
		$actionsMap = array('-1' => _t('LeftAndMain.DropdownBatchActionsDefault', 'Choose an action...')); // Placeholder action
1546
		foreach($actions as $action) {
1547
			$actionsMap[$action->Link] = $action->Title;
1548
		}
1549
1550
		$form = new Form(
1551
			$this,
1552
			'BatchActionsForm',
1553
			new FieldList(
1554
				new HiddenField('csvIDs'),
1555
				DropdownField::create(
1556
					'Action',
1557
					false,
1558
					$actionsMap
1559
				)
1560
					->setAttribute('autocomplete', 'off')
1561
					->setAttribute('data-placeholder', _t('LeftAndMain.DropdownBatchActionsDefault', 'Choose an action...'))
1562
			),
1563
			new FieldList(
1564
				// TODO i18n
1565
				new FormAction('submit', _t('Form.SubmitBtnLabel', "Go"))
1566
			)
1567
		);
1568
		$form->addExtraClass('cms-batch-actions nostyle');
1569
		$form->unsetValidator();
1570
1571
		$this->extend('updateBatchActionsForm', $form);
1572
		return $form;
1573
	}
1574
1575
	public function printable() {
1576
		$form = $this->getEditForm($this->currentPageID());
1577
		if(!$form) return false;
1578
1579
		$form->transform(new PrintableTransformation());
1580
		$form->setActions(null);
0 ignored issues
show
Documentation introduced by
null is of type null, but the function expects a object<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...
1581
1582
		Requirements::clear();
1583
		Requirements::css(FRAMEWORK_ADMIN_DIR . '/css/LeftAndMain_printable.css');
1584
		return array(
1585
			"PrintForm" => $form
1586
		);
1587
	}
1588
1589
	/**
1590
	 * Used for preview controls, mainly links which switch between different states of the page.
1591
	 *
1592
	 * @return ArrayData
1593
	 */
1594
	public function getSilverStripeNavigator() {
1595
		$page = $this->currentPage();
1596
		if($page) {
1597
			$navigator = new SilverStripeNavigator($page);
1598
			return $navigator->renderWith($this->getTemplatesWithSuffix('_SilverStripeNavigator'));
1599
		} else {
1600
			return false;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return false; (false) is incompatible with the return type documented by LeftAndMain::getSilverStripeNavigator of type ArrayData.

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...
1601
		}
1602
	}
1603
1604
	/**
1605
	 * Identifier for the currently shown record,
1606
	 * in most cases a database ID. Inspects the following
1607
	 * sources (in this order):
1608
	 * - GET/POST parameter named 'ID'
1609
	 * - URL parameter named 'ID'
1610
	 * - Session value namespaced by classname, e.g. "CMSMain.currentPage"
1611
	 *
1612
	 * @return int
1613
	 */
1614
	public function currentPageID() {
1615
		if($this->getRequest()->requestVar('ID') && is_numeric($this->getRequest()->requestVar('ID')))	{
1616
			return $this->getRequest()->requestVar('ID');
1617
		} elseif (isset($this->urlParams['ID']) && is_numeric($this->urlParams['ID'])) {
1618
			return $this->urlParams['ID'];
1619
		} elseif(Session::get($this->sessionNamespace() . ".currentPage")) {
1620
			return Session::get($this->sessionNamespace() . ".currentPage");
1621
		} else {
1622
			return null;
1623
		}
1624
	}
1625
1626
	/**
1627
	 * Forces the current page to be set in session,
1628
	 * which can be retrieved later through {@link currentPageID()}.
1629
	 * Keep in mind that setting an ID through GET/POST or
1630
	 * as a URL parameter will overrule this value.
1631
	 *
1632
	 * @param int $id
1633
	 */
1634
	public function setCurrentPageID($id) {
1635
		Session::set($this->sessionNamespace() . ".currentPage", $id);
1636
	}
1637
1638
	/**
1639
	 * Uses {@link getRecord()} and {@link currentPageID()}
1640
	 * to get the currently selected record.
1641
	 *
1642
	 * @return DataObject
1643
	 */
1644
	public function currentPage() {
1645
		return $this->getRecord($this->currentPageID());
1646
	}
1647
1648
	/**
1649
	 * Compares a given record to the currently selected one (if any).
1650
	 * Used for marking the current tree node.
1651
	 *
1652
	 * @return boolean
1653
	 */
1654
	public function isCurrentPage(DataObject $record) {
1655
		return ($record->ID == $this->currentPageID());
1656
	}
1657
1658
	/**
1659
	 * @return String
1660
	 */
1661
	protected function sessionNamespace() {
1662
		$override = $this->stat('session_namespace');
1663
		return $override ? $override : $this->class;
1664
	}
1665
1666
	/**
1667
	 * URL to a previewable record which is shown through this controller.
1668
	 * The controller might not have any previewable content, in which case
1669
	 * this method returns FALSE.
1670
	 *
1671
	 * @return String|boolean
1672
	 */
1673
	public function LinkPreview() {
1674
		return false;
1675
	}
1676
1677
	/**
1678
	 * Return the version number of this application.
1679
	 * Uses the number in <mymodule>/silverstripe_version
1680
	 * (automatically replaced by build scripts).
1681
	 * If silverstripe_version is empty,
1682
	 * then attempts to get it from composer.lock
1683
	 *
1684
	 * @return string
1685
	 */
1686
	public function CMSVersion() {
1687
		$versions = array();
1688
		$modules = array(
1689
			'silverstripe/framework' => array(
1690
				'title' => 'Framework',
1691
				'versionFile' => FRAMEWORK_PATH . '/silverstripe_version',
1692
			)
1693
		);
1694
		if(defined('CMS_PATH')) {
1695
			$modules['silverstripe/cms'] = array(
1696
				'title' => 'CMS',
1697
				'versionFile' => CMS_PATH . '/silverstripe_version',
1698
			);
1699
		}
1700
1701
		// Tries to obtain version number from composer.lock if it exists
1702
		$composerLockPath = BASE_PATH . '/composer.lock';
1703
		if (file_exists($composerLockPath)) {
1704
			$cache = SS_Cache::factory('LeftAndMain_CMSVersion');
1705
			$cacheKey = filemtime($composerLockPath);
1706
			$versions = $cache->load($cacheKey);
1707
			if($versions) {
1708
				$versions = json_decode($versions, true);
1709
			} else {
1710
				$versions = array();
1711
			}
1712
			if(!$versions && $jsonData = file_get_contents($composerLockPath)) {
1713
				$lockData = json_decode($jsonData);
1714
				if($lockData && isset($lockData->packages)) {
1715
					foreach ($lockData->packages as $package) {
1716
						if(
1717
							array_key_exists($package->name, $modules)
1718
							&& isset($package->version)
1719
						) {
1720
							$versions[$package->name] = $package->version;
1721
						}
1722
					}
1723
					$cache->save(json_encode($versions), $cacheKey);
1724
				}
1725
			}
1726
		}
1727
1728
		// Fall back to static version file
1729
		foreach($modules as $moduleName => $moduleSpec) {
1730
			if(!isset($versions[$moduleName])) {
1731
				if($staticVersion = file_get_contents($moduleSpec['versionFile'])) {
1732
					$versions[$moduleName] = $staticVersion;
1733
				} else {
1734
					$versions[$moduleName] = _t('LeftAndMain.VersionUnknown', 'Unknown');
1735
				}
1736
			}
1737
		}
1738
1739
		$out = array();
1740
		foreach($modules as $moduleName => $moduleSpec) {
1741
			$out[] = $modules[$moduleName]['title'] . ': ' . $versions[$moduleName];
1742
		}
1743
		return implode(', ', $out);
1744
	}
1745
1746
	/**
1747
	 * @return array
1748
	 */
1749
	public function SwitchView() {
1750
		if($page = $this->currentPage()) {
1751
			$nav = SilverStripeNavigator::get_for_record($page);
1752
			return $nav['items'];
1753
		}
1754
	}
1755
1756
	/**
1757
	 * @return SiteConfig
1758
	 */
1759
	public function SiteConfig() {
1760
		return (class_exists('SiteConfig')) ? SiteConfig::current_site_config() : null;
1761
	}
1762
1763
	/**
1764
	 * The href for the anchor on the Silverstripe logo.
1765
	 * Set by calling LeftAndMain::set_application_link()
1766
	 *
1767
	 * @config
1768
	 * @var String
1769
	 */
1770
	private static $application_link = '//www.silverstripe.org/';
1771
1772
	/**
1773
	 * Sets the href for the anchor on the Silverstripe logo in the menu
1774
	 *
1775
	 * @deprecated since version 4.0
1776
	 *
1777
	 * @param String $link
1778
	 */
1779
	public static function set_application_link($link) {
1780
		Deprecation::notice('4.0', 'Use the "LeftAndMain.application_link" config setting instead');
1781
		Config::inst()->update('LeftAndMain', 'application_link', $link);
1782
	}
1783
1784
	/**
1785
	 * @return String
1786
	 */
1787
	public function ApplicationLink() {
1788
		return $this->stat('application_link');
1789
	}
1790
1791
	/**
1792
	 * The application name. Customisable by calling
1793
	 * LeftAndMain::setApplicationName() - the first parameter.
1794
	 *
1795
	 * @config
1796
	 * @var String
1797
	 */
1798
	private static $application_name = 'SilverStripe';
1799
1800
	/**
1801
	 * @param String $name
1802
	 * @deprecated since version 4.0
1803
	 */
1804
	public static function setApplicationName($name) {
1805
		Deprecation::notice('4.0', 'Use the "LeftAndMain.application_name" config setting instead');
1806
		Config::inst()->update('LeftAndMain', 'application_name', $name);
1807
	}
1808
1809
	/**
1810
	 * Get the application name.
1811
	 *
1812
	 * @return string
1813
	 */
1814
	public function getApplicationName() {
1815
		return $this->stat('application_name');
1816
	}
1817
1818
	/**
1819
	 * @return string
1820
	 */
1821
	public function Title() {
1822
		$app = $this->getApplicationName();
1823
1824
		return ($section = $this->SectionTitle()) ? sprintf('%s - %s', $app, $section) : $app;
1825
	}
1826
1827
	/**
1828
	 * Return the title of the current section. Either this is pulled from
1829
	 * the current panel's menu_title or from the first active menu
1830
	 *
1831
	 * @return string
1832
	 */
1833
	public function SectionTitle() {
1834
		$class = get_class($this);
1835
		$defaultTitle = LeftAndMain::menu_title_for_class($class);
1836
		if($title = _t("{$class}.MENUTITLE", $defaultTitle)) return $title;
1837
1838
		foreach($this->MainMenu() as $menuItem) {
1839
			if($menuItem->LinkingMode != 'link') return $menuItem->Title;
1840
		}
1841
	}
1842
1843
	/**
1844
	 * Same as {@link ViewableData->CSSClasses()}, but with a changed name
1845
	 * to avoid problems when using {@link ViewableData->customise()}
1846
	 * (which always returns "ArrayData" from the $original object).
1847
	 *
1848
	 * @return String
1849
	 */
1850
	public function BaseCSSClasses() {
1851
		return $this->CSSClasses('Controller');
1852
	}
1853
1854
	/**
1855
	 * @return String
1856
	 */
1857
	public function Locale() {
1858
		return DBField::create_field('Locale', i18n::get_locale());
1859
	}
1860
1861
	public function providePermissions() {
1862
		$perms = array(
1863
			"CMS_ACCESS_LeftAndMain" => array(
1864
				'name' => _t('CMSMain.ACCESSALLINTERFACES', 'Access to all CMS sections'),
1865
				'category' => _t('Permission.CMS_ACCESS_CATEGORY', 'CMS Access'),
1866
				'help' => _t('CMSMain.ACCESSALLINTERFACESHELP', 'Overrules more specific access settings.'),
1867
				'sort' => -100
1868
			)
1869
		);
1870
1871
		// Add any custom ModelAdmin subclasses. Can't put this on ModelAdmin itself
1872
		// since its marked abstract, and needs to be singleton instanciated.
1873
		foreach(ClassInfo::subclassesFor('ModelAdmin') as $i => $class) {
0 ignored issues
show
Bug introduced by
The expression \ClassInfo::subclassesFor('ModelAdmin') of type null|array is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

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

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

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

Loading history...
1874
			if($class == 'ModelAdmin') continue;
1875
			if(ClassInfo::classImplements($class, 'TestOnly')) continue;
1876
1877
			$title = _t("{$class}.MENUTITLE", LeftAndMain::menu_title_for_class($class));
1878
			$perms["CMS_ACCESS_" . $class] = array(
1879
				'name' => _t(
1880
					'CMSMain.ACCESS',
1881
					"Access to '{title}' section",
1882
					"Item in permission selection identifying the admin section. Example: Access to 'Files & Images'",
1883
					array('title' => $title)
0 ignored issues
show
Documentation introduced by
array('title' => $title) is of type array<string,string,{"title":"string"}>, but the function expects a string.

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1884
				),
1885
				'category' => _t('Permission.CMS_ACCESS_CATEGORY', 'CMS Access')
1886
			);
1887
		}
1888
1889
		return $perms;
1890
	}
1891
1892
	/**
1893
	 * Register the given javascript file as required in the CMS.
1894
	 * Filenames should be relative to the base, eg, FRAMEWORK_DIR . '/javascript/dist/loader.js'
1895
	 *
1896
	 * @deprecated since version 4.0
1897
	 */
1898
	public static function require_javascript($file) {
1899
		Deprecation::notice('4.0', 'Use "LeftAndMain.extra_requirements_javascript" config setting instead');
1900
		Config::inst()->update('LeftAndMain', 'extra_requirements_javascript', array($file => array()));
1901
	}
1902
1903
	/**
1904
	 * Register the given stylesheet file as required.
1905
	 * @deprecated since version 4.0
1906
	 *
1907
	 * @param $file String Filenames should be relative to the base, eg, THIRDPARTY_DIR . '/tree/tree.css'
1908
	 * @param $media String Comma-separated list of media-types (e.g. "screen,projector")
1909
	 * @see http://www.w3.org/TR/REC-CSS2/media.html
1910
	 */
1911
	public static function require_css($file, $media = null) {
1912
		Deprecation::notice('4.0', 'Use "LeftAndMain.extra_requirements_css" config setting instead');
1913
		Config::inst()->update('LeftAndMain', 'extra_requirements_css', array($file => array('media' => $media)));
1914
	}
1915
1916
	/**
1917
	 * Register the given "themeable stylesheet" as required.
1918
	 * Themeable stylesheets have globally unique names, just like templates and PHP files.
1919
	 * Because of this, they can be replaced by similarly named CSS files in the theme directory.
1920
	 *
1921
	 * @deprecated since version 4.0
1922
	 *
1923
	 * @param $name String The identifier of the file.  For example, css/MyFile.css would have the identifier "MyFile"
1924
	 * @param $media String Comma-separated list of media-types (e.g. "screen,projector")
1925
	 */
1926
	public static function require_themed_css($name, $media = null) {
1927
		Deprecation::notice('4.0', 'Use "LeftAndMain.extra_requirements_themedCss" config setting instead');
1928
		Config::inst()->update('LeftAndMain', 'extra_requirements_themedCss', array($name => array('media' => $media)));
1929
	}
1930
1931
}
1932
1933
/**
1934
 * @package cms
1935
 * @subpackage core
1936
 */
1937
class LeftAndMainMarkingFilter {
1938
1939
	/**
1940
	 * @var array Request params (unsanitized)
1941
	 */
1942
	protected $params = array();
1943
1944
	/**
1945
	 * @param array $params Request params (unsanitized)
1946
	 */
1947
	public function __construct($params = null) {
1948
		$this->ids = array();
0 ignored issues
show
Bug introduced by
The property ids does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
1949
		$this->expanded = array();
0 ignored issues
show
Bug introduced by
The property expanded does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
1950
		$parents = array();
1951
1952
		$q = $this->getQuery($params);
1953
		$res = $q->execute();
1954
		if (!$res) return;
1955
1956
		// And keep a record of parents we don't need to get parents
1957
		// of themselves, as well as IDs to mark
1958
		foreach($res as $row) {
1959
			if ($row['ParentID']) $parents[$row['ParentID']] = true;
1960
			$this->ids[$row['ID']] = true;
1961
		}
1962
1963
		// We need to recurse up the tree,
1964
		// finding ParentIDs for each ID until we run out of parents
1965
		while (!empty($parents)) {
1966
			$parentsClause = DB::placeholders($parents);
1967
			$res = DB::prepared_query(
1968
				"SELECT \"ParentID\", \"ID\" FROM \"SiteTree\" WHERE \"ID\" in ($parentsClause)",
1969
				array_keys($parents)
1970
			);
1971
			$parents = array();
1972
1973
			foreach($res as $row) {
1974
				if ($row['ParentID']) $parents[$row['ParentID']] = true;
1975
				$this->ids[$row['ID']] = true;
1976
				$this->expanded[$row['ID']] = true;
1977
			}
1978
		}
1979
	}
1980
1981
	protected function getQuery($params) {
1982
		$where = array();
1983
1984
		if(isset($params['ID'])) unset($params['ID']);
1985
		if($treeClass = static::config()->tree_class) foreach($params as $name => $val) {
1986
			// Partial string match against a variety of fields
1987
			if(!empty($val) && singleton($treeClass)->hasDatabaseField($name)) {
1988
				$predicate = sprintf('"%s" LIKE ?', $name);
1989
				$where[$predicate] = "%$val%";
1990
			}
1991
		}
1992
1993
		return new SQLSelect(
1994
			array("ParentID", "ID"),
1995
			'SiteTree',
1996
			$where
1997
		);
1998
	}
1999
2000
	public function mark($node) {
2001
		$id = $node->ID;
2002
		if(array_key_exists((int) $id, $this->expanded)) $node->markOpened();
2003
		return array_key_exists((int) $id, $this->ids) ? $this->ids[$id] : false;
2004
	}
2005
}
2006
2007
/**
2008
 * Allow overriding finished state for faux redirects.
2009
 *
2010
 * @package framework
2011
 * @subpackage admin
2012
 */
2013
class LeftAndMain_HTTPResponse extends SS_HTTPResponse {
2014
2015
	protected $isFinished = false;
2016
2017
	public function isFinished() {
2018
		return (parent::isFinished() || $this->isFinished);
2019
	}
2020
2021
	public function setIsFinished($bool) {
2022
		$this->isFinished = $bool;
2023
	}
2024
2025
}
2026
2027
/**
2028
 * Wrapper around objects being displayed in a tree.
2029
 * Caution: Volatile API.
2030
 *
2031
 * @todo Implement recursive tree node rendering.
2032
 *
2033
 * @package framework
2034
 * @subpackage admin
2035
 */
2036
class LeftAndMain_TreeNode extends ViewableData {
2037
2038
	/**
2039
	 * Object represented by this node
2040
	 *
2041
	 * @var Object
2042
	 */
2043
	protected $obj;
2044
2045
	/**
2046
	 * Edit link to the current record in the CMS
2047
	 *
2048
	 * @var string
2049
	 */
2050
	protected $link;
2051
2052
	/**
2053
	 * True if this is the currently selected node in the tree
2054
	 *
2055
	 * @var bool
2056
	 */
2057
	protected $isCurrent;
2058
2059
	/**
2060
	 * Name of method to count the number of children
2061
	 *
2062
	 * @var string
2063
	 */
2064
	protected $numChildrenMethod;
2065
2066
2067
	/**
2068
	 *
2069
	 * @var LeftAndMain_SearchFilter
2070
	 */
2071
	protected $filter;
2072
2073
	/**
2074
	 * @param Object $obj
2075
	 * @param string $link
2076
	 * @param bool $isCurrent
2077
	 * @param string $numChildrenMethod
2078
	 * @param LeftAndMain_SearchFilter $filter
2079
	 */
2080
	public function __construct($obj, $link = null, $isCurrent = false,
2081
		$numChildrenMethod = 'numChildren', $filter = null
2082
	) {
2083
		parent::__construct();
2084
		$this->obj = $obj;
2085
		$this->link = $link;
2086
		$this->isCurrent = $isCurrent;
2087
		$this->numChildrenMethod = $numChildrenMethod;
2088
		$this->filter = $filter;
2089
	}
2090
2091
	/**
2092
	 * Returns template, for further processing by {@link Hierarchy->getChildrenAsUL()}.
2093
	 * Does not include closing tag to allow this method to inject its own children.
2094
	 *
2095
	 * @todo Remove hardcoded assumptions around returning an <li>, by implementing recursive tree node rendering
2096
	 *
2097
	 * @return String
2098
	 */
2099
	public function forTemplate() {
2100
		$obj = $this->obj;
2101
		return "<li id=\"record-$obj->ID\" data-id=\"$obj->ID\" data-pagetype=\"$obj->ClassName\" class=\""
2102
			. $this->getClasses() . "\">" . "<ins class=\"jstree-icon\">&nbsp;</ins>"
2103
			. "<a href=\"" . $this->getLink() . "\" title=\"("
2104
			. trim(_t('LeftAndMain.PAGETYPE','Page type'), " :") // account for inconsistencies in translations
2105
			. ": " . $obj->i18n_singular_name() . ") $obj->Title\" ><ins class=\"jstree-icon\">&nbsp;</ins><span class=\"text\">" . ($obj->TreeTitle)
2106
			. "</span></a>";
2107
	}
2108
2109
	/**
2110
	 * Determine the CSS classes to apply to this node
2111
	 *
2112
	 * @return string
2113
	 */
2114
	public function getClasses() {
2115
		// Get classes from object
2116
		$classes = $this->obj->CMSTreeClasses($this->numChildrenMethod);
2117
		if($this->isCurrent) {
2118
			$classes .= ' current';
2119
		}
2120
		// Get status flag classes
2121
		$flags = $this->obj->hasMethod('getStatusFlags')
2122
			? $this->obj->getStatusFlags()
2123
			: false;
2124
		if ($flags) {
2125
			$statuses = array_keys($flags);
2126
			foreach ($statuses as $s) {
2127
				$classes .= ' status-' . $s;
2128
			}
2129
		}
2130
		// Get additional filter classes
2131
		if($this->filter && ($filterClasses = $this->filter->getPageClasses($this->obj))) {
2132
			if(is_array($filterClasses)) {
2133
				$filterClasses = implode(' ' . $filterClasses);
2134
			}
2135
			$classes .= ' ' . $filterClasses;
2136
		}
2137
		return $classes;
2138
	}
2139
2140
	public function getObj() {
2141
		return $this->obj;
2142
	}
2143
2144
	public function setObj($obj) {
2145
		$this->obj = $obj;
2146
		return $this;
2147
	}
2148
2149
	public function getLink() {
2150
		return $this->link;
2151
	}
2152
2153
	public function setLink($link) {
2154
		$this->link = $link;
2155
		return $this;
2156
	}
2157
2158
	public function getIsCurrent() {
2159
		return $this->isCurrent;
2160
	}
2161
2162
	public function setIsCurrent($bool) {
2163
		$this->isCurrent = $bool;
2164
		return $this;
2165
	}
2166
2167
}
2168
2169
/**
2170
 * Abstract interface for a class which may be used to filter the results displayed
2171
 * in a nested tree
2172
 */
2173
interface LeftAndMain_SearchFilter {
2174
2175
	/**
2176
	 * Method on {@link Hierarchy} objects which is used to traverse into children relationships.
2177
	 *
2178
	 * @return string
2179
	 */
2180
	public function getChildrenMethod();
2181
2182
	/**
2183
	 * Method on {@link Hierarchy} objects which is used find the number of children for a parent page
2184
	 *
2185
	 * @return string
2186
	 */
2187
	public function getNumChildrenMethod();
2188
2189
2190
	/**
2191
	 * Returns TRUE if the given page should be included in the tree.
2192
	 * Caution: Does NOT check view permissions on the page.
2193
	 *
2194
	 * @param DataObject $page
2195
	 * @return bool
2196
	 */
2197
	public function isPageIncluded($page);
2198
2199
	/**
2200
	 * Given a page, determine any additional CSS classes to apply to the tree node
2201
	 *
2202
	 * @param DataObject $page
2203
	 * @return array|string
2204
	 */
2205
	public function getPageClasses($page);
2206
}
2207