Completed
Push — hash-nonce ( 07e2e8 )
by Sam
08:52
created

LeftAndMain   F

Complexity

Total Complexity 283

Size/Duplication

Total Lines 1800
Duplicated Lines 2.78 %

Coupling/Cohesion

Components 1
Dependencies 45

Importance

Changes 2
Bugs 2 Features 1
Metric Value
dl 50
loc 1800
rs 0.5217
c 2
b 2
f 1
wmc 283
lcom 1
cbo 45

59 Methods

Rating   Name   Duplication   Size   Complexity  
D savetreenode() 0 97 21
A setCurrentPageID() 0 4 1
C canView() 0 26 11
F init() 20 243 44
B handleRequest() 0 26 4
B redirect() 0 22 5
A index() 0 3 1
A ShowSwitchView() 0 3 1
A Link() 0 17 2
A menu_title_for_class() 0 5 2
A menu_icon_for_class() 0 8 2
A show() 0 5 2
A getResponseNegotiator() 0 23 2
C MainMenu() 0 79 19
A Menu() 0 3 1
A MenuCurrentItem() 0 4 1
A getTemplatesWithSuffix() 0 3 1
A Content() 0 3 1
B getRecord() 0 12 7
D Breadcrumbs() 0 31 10
A SiteTreeAsUL() 0 5 1
A getSearchFilter() 0 16 3
F getSiteTreeFor() 0 124 27
A getsubtree() 0 16 1
C updatetreenodes() 0 44 8
C save() 0 23 7
B delete() 0 16 5
A CanOrganiseSitetree() 0 3 3
A EditForm() 0 3 1
F getEditForm() 12 116 26
B EmptyForm() 0 29 1
A EditorToolbar() 0 3 1
A Tools() 9 9 2
A EditFormTools() 9 9 2
A batchactions() 0 3 1
B BatchActionsForm() 0 31 2
A printable() 0 13 2
A getSilverStripeNavigator() 0 9 2
B currentPageID() 0 11 6
A currentPage() 0 3 1
A isCurrentPage() 0 3 1
A sessionNamespace() 0 4 2
A LinkPreview() 0 3 1
C CMSVersion() 0 59 15
A SwitchView() 0 6 2
A SiteConfig() 0 3 2
A set_application_link() 0 4 1
A ApplicationLink() 0 3 1
A setApplicationName() 0 4 1
A getApplicationName() 0 3 1
A Title() 0 5 2
A SectionTitle() 0 9 4
A MceRoot() 0 3 1
A BaseCSSClasses() 0 3 1
A Locale() 0 3 1
B providePermissions() 0 30 4
A require_javascript() 0 4 1
A require_css() 0 4 1
A require_themed_css() 0 4 1

How to fix   Duplicated Code    Complexity   

Duplicated Code

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

Common duplication problems, and corresponding solutions are:

Complex Class

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

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

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

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

1
<?php
2
3
/**
4
 * @package framework
5
 * @subpackage admin
6
 */
7
8
/**
9
 * LeftAndMain is the parent class of all the two-pane views in the CMS.
10
 * If you are wanting to add more areas to the CMS, you can do it by subclassing LeftAndMain.
11
 *
12
 * This is essentially an abstract class which should be subclassed.
13
 * See {@link CMSMain} for a good example.
14
 */
15
class LeftAndMain extends Controller implements PermissionProvider {
16
17
	/**
18
	 * The 'base' url for CMS administration areas.
19
	 * Note that if this is changed, many javascript
20
	 * behaviours need to be updated with the correct url
21
	 *
22
	 * @config
23
	 * @var string $url_base
24
	 */
25
	private static $url_base = "admin";
26
27
	/**
28
	 * The current url segment attached to the LeftAndMain instance
29
	 *
30
	 * @config
31
	 * @var string
32
	 */
33
	private static $url_segment;
34
35
	/**
36
	 * @config
37
	 * @var string
38
	 */
39
	private static $url_rule = '/$Action/$ID/$OtherID';
40
41
	/**
42
	 * @config
43
	 * @var string
44
	 */
45
	private static $menu_title;
46
47
	/**
48
	 * @config
49
	 * @var string
50
	 */
51
	private static $menu_icon;
52
53
	/**
54
	 * @config
55
	 * @var int
56
	 */
57
	private static $menu_priority = 0;
58
59
	/**
60
	 * @config
61
	 * @var int
62
	 */
63
	private static $url_priority = 50;
64
65
	/**
66
	 * A subclass of {@link DataObject}.
67
	 *
68
	 * Determines what is managed in this interface, through
69
	 * {@link getEditForm()} and other logic.
70
	 *
71
	 * @config
72
	 * @var string
73
	 */
74
	private static $tree_class = null;
75
76
	/**
77
	 * The url used for the link in the Help tab in the backend
78
	 *
79
	 * @config
80
	 * @var string
81
	 */
82
	private static $help_link = '//userhelp.silverstripe.org/framework/en/3.3';
83
84
	/**
85
	 * @var array
86
	 */
87
	private static $allowed_actions = array(
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...
88
		'index',
89
		'save',
90
		'savetreenode',
91
		'getsubtree',
92
		'updatetreenodes',
93
		'printable',
94
		'show',
95
		'EditorToolbar',
96
		'EditForm',
97
		'AddForm',
98
		'batchactions',
99
		'BatchActionsForm',
100
	);
101
102
	/**
103
	 * @config
104
	 * @var Array Codes which are required from the current user to view this controller.
105
	 * If multiple codes are provided, all of them are required.
106
	 * All CMS controllers require "CMS_ACCESS_LeftAndMain" as a baseline check,
107
	 * and fall back to "CMS_ACCESS_<class>" if no permissions are defined here.
108
	 * See {@link canView()} for more details on permission checks.
109
	 */
110
	private static $required_permission_codes;
111
112
	/**
113
	 * @config
114
	 * @var String Namespace for session info, e.g. current record.
115
	 * Defaults to the current class name, but can be amended to share a namespace in case
116
	 * controllers are logically bundled together, and mainly separated
117
	 * to achieve more flexible templating.
118
	 */
119
	private static $session_namespace;
120
121
	/**
122
	 * Register additional requirements through the {@link Requirements} class.
123
	 * Used mainly to work around the missing "lazy loading" functionality
124
	 * for getting css/javascript required after an ajax-call (e.g. loading the editform).
125
	 *
126
	 * YAML configuration example:
127
	 * <code>
128
	 * LeftAndMain:
129
	 *   extra_requirements_javascript:
130
	 *     mysite/javascript/myscript.js:
131
	 * </code>
132
	 *
133
	 * @config
134
	 * @var array
135
	 */
136
	private static $extra_requirements_javascript = array();
137
138
	/**
139
	 * YAML configuration example:
140
	 * <code>
141
	 * LeftAndMain:
142
	 *   extra_requirements_css:
143
	 *     mysite/css/mystyle.css:
144
	 *       media: screen
145
	 * </code>
146
	 *
147
	 * @config
148
	 * @var array See {@link extra_requirements_javascript}
149
	 */
150
	private static $extra_requirements_css = array();
151
152
	/**
153
	 * @config
154
	 * @var array See {@link extra_requirements_javascript}
155
	 */
156
	private static $extra_requirements_themedCss = array();
157
158
	/**
159
	 * If true, call a keepalive ping every 5 minutes from the CMS interface,
160
	 * to ensure that the session never dies.
161
	 *
162
	 * @config
163
	 * @var boolean
164
	 */
165
	private static $session_keepalive_ping = true;
166
167
	/**
168
	 * @var PjaxResponseNegotiator
169
	 */
170
	protected $responseNegotiator;
171
172
	/**
173
	 * @param Member $member
174
	 * @return boolean
175
	 */
176
	public function canView($member = null) {
177
		if(!$member && $member !== FALSE) $member = Member::currentUser();
178
179
		// cms menus only for logged-in members
180
		if(!$member) return false;
181
182
		// alternative extended checks
183
		if($this->hasMethod('alternateAccessCheck')) {
184
			$alternateAllowed = $this->alternateAccessCheck();
185
			if($alternateAllowed === FALSE) return false;
186
		}
187
188
		// Check for "CMS admin" permission
189
		if(Permission::checkMember($member, "CMS_ACCESS_LeftAndMain")) return true;
190
191
		// Check for LeftAndMain sub-class permissions
192
		$codes = array();
193
		$extraCodes = $this->stat('required_permission_codes');
194
		if($extraCodes !== false) { // allow explicit FALSE to disable subclass check
195
			if($extraCodes) $codes = array_merge($codes, (array)$extraCodes);
196
			else $codes[] = "CMS_ACCESS_$this->class";
197
		}
198
		foreach($codes as $code) if(!Permission::checkMember($member, $code)) return false;
199
200
		return true;
201
	}
202
203
	/**
204
	 * @uses LeftAndMainExtension->init()
205
	 * @uses LeftAndMainExtension->accessedCMS()
206
	 * @uses CMSMenu
207
	 */
208
	public function init() {
209
		parent::init();
210
211
		Config::inst()->update('SSViewer', 'rewrite_hash_links', false);
212
		Config::inst()->update('ContentNegotiator', 'enabled', false);
213
214
		// set language
215
		$member = Member::currentUser();
216
		if(!empty($member->Locale)) i18n::set_locale($member->Locale);
217
		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...
218
		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...
219
220
		// can't be done in cms/_config.php as locale is not set yet
221
		CMSMenu::add_link(
222
			'Help',
223
			_t('LeftAndMain.HELP', 'Help', 'Menu title'),
224
			$this->config()->help_link,
225
			-2,
226
			array(
227
				'target' => '_blank'
228
			)
229
		);
230
231
		// Allow customisation of the access check by a extension
232
		// Also all the canView() check to execute Controller::redirect()
233
		if(!$this->canView() && !$this->getResponse()->isFinished()) {
234
			// When access /admin/, we should try a redirect to another part of the admin rather than be locked out
235
			$menu = $this->MainMenu();
236
			foreach($menu as $candidate) {
237
				if(
238
					$candidate->Link &&
239
					$candidate->Link != $this->Link()
240
					&& $candidate->MenuItem->controller
241
					&& singleton($candidate->MenuItem->controller)->canView()
242
				) {
243
					return $this->redirect($candidate->Link);
244
				}
245
			}
246
247
			if(Member::currentUser()) {
248
				Session::set("BackURL", null);
249
			}
250
251
			// if no alternate menu items have matched, return a permission error
252
			$messageSet = array(
253
				'default' => _t(
254
					'LeftAndMain.PERMDEFAULT',
255
					"You must be logged in to access the administration area; please enter your credentials below."
256
				),
257
				'alreadyLoggedIn' => _t(
258
					'LeftAndMain.PERMALREADY',
259
					"I'm sorry, but you can't access that part of the CMS.  If you want to log in as someone else, do"
260
					. " so below."
261
				),
262
				'logInAgain' => _t(
263
					'LeftAndMain.PERMAGAIN',
264
					"You have been logged out of the CMS.  If you would like to log in again, enter a username and"
265
					. " password below."
266
				),
267
			);
268
269
			return Security::permissionFailure($this, $messageSet);
270
		}
271
272
		// Don't continue if there's already been a redirection request.
273
		if($this->redirectedTo()) return;
274
275
		// Audit logging hook
276
		if(empty($_REQUEST['executeForm']) && !$this->getRequest()->isAjax()) $this->extend('accessedCMS');
277
278
		// Set the members html editor config
279
		if(Member::currentUser()) {
280
			HtmlEditorConfig::set_active(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...
281
		}
282
283
		// Set default values in the config if missing.  These things can't be defined in the config
284
		// file because insufficient information exists when that is being processed
285
		$htmlEditorConfig = HtmlEditorConfig::get_active();
286
		$htmlEditorConfig->setOption('language', i18n::get_tinymce_lang());
287
		if(!$htmlEditorConfig->getOption('content_css')) {
288
			$cssFiles = array();
289
			$cssFiles[] = FRAMEWORK_ADMIN_DIR . '/css/editor.css';
290
291
			// Use theme from the site config
292
			if(class_exists('SiteConfig') && ($config = SiteConfig::current_site_config()) && $config->Theme) {
293
				$theme = $config->Theme;
294
			} elseif(Config::inst()->get('SSViewer', 'theme_enabled') && Config::inst()->get('SSViewer', 'theme')) {
295
				$theme = Config::inst()->get('SSViewer', 'theme');
296
			} else {
297
				$theme = false;
298
			}
299
300
			if($theme) $cssFiles[] = THEMES_DIR . "/{$theme}/css/editor.css";
301
			else if(project()) $cssFiles[] = project() . '/css/editor.css';
302
303
			// Remove files that don't exist
304
			foreach($cssFiles as $k => $cssFile) {
305
				if(!file_exists(BASE_PATH . '/' . $cssFile)) unset($cssFiles[$k]);
306
			}
307
308
			$htmlEditorConfig->setOption('content_css', implode(',', $cssFiles));
309
		}
310
311
		// Using uncompressed files as they'll be processed by JSMin in the Requirements class.
312
		// Not as effective as other compressors or pre-compressed+finetuned files,
313
		// but overall the unified minification into a single file brings more performance benefits
314
		// than a couple of saved bytes (after gzip) in individual files.
315
		// We also re-compress already compressed files through JSMin as this causes weird runtime bugs.
316
		Requirements::combine_files(
317
			'lib.js',
318
			array(
319
				THIRDPARTY_DIR . '/jquery/jquery.js',
320
				FRAMEWORK_DIR . '/javascript/jquery-ondemand/jquery.ondemand.js',
321
				FRAMEWORK_ADMIN_DIR . '/javascript/lib.js',
322
				THIRDPARTY_DIR . '/jquery-ui/jquery-ui.js',
323
				THIRDPARTY_DIR . '/json-js/json2.js',
324
				THIRDPARTY_DIR . '/jquery-entwine/dist/jquery.entwine-dist.js',
325
				THIRDPARTY_DIR . '/jquery-cookie/jquery.cookie.js',
326
				THIRDPARTY_DIR . '/jquery-query/jquery.query.js',
327
				THIRDPARTY_DIR . '/jquery-form/jquery.form.js',
328
				FRAMEWORK_ADMIN_DIR . '/thirdparty/jquery-notice/jquery.notice.js',
329
				FRAMEWORK_ADMIN_DIR . '/thirdparty/jsizes/lib/jquery.sizes.js',
330
				FRAMEWORK_ADMIN_DIR . '/thirdparty/jlayout/lib/jlayout.border.js',
331
				FRAMEWORK_ADMIN_DIR . '/thirdparty/jlayout/lib/jquery.jlayout.js',
332
				FRAMEWORK_ADMIN_DIR . '/thirdparty/history-js/scripts/uncompressed/history.js',
333
				FRAMEWORK_ADMIN_DIR . '/thirdparty/history-js/scripts/uncompressed/history.adapter.jquery.js',
334
				FRAMEWORK_ADMIN_DIR . '/thirdparty/history-js/scripts/uncompressed/history.html4.js',
335
				THIRDPARTY_DIR . '/jstree/jquery.jstree.js',
336
				FRAMEWORK_ADMIN_DIR . '/thirdparty/chosen/chosen/chosen.jquery.js',
337
				FRAMEWORK_ADMIN_DIR . '/thirdparty/jquery-hoverIntent/jquery.hoverIntent.js',
338
				FRAMEWORK_ADMIN_DIR . '/javascript/jquery-changetracker/lib/jquery.changetracker.js',
339
				FRAMEWORK_DIR . '/javascript/i18n.js',
340
				FRAMEWORK_DIR . '/javascript/TreeDropdownField.js',
341
				FRAMEWORK_DIR . '/javascript/DateField.js',
342
				FRAMEWORK_DIR . '/javascript/HtmlEditorField.js',
343
				FRAMEWORK_DIR . '/javascript/TabSet.js',
344
				FRAMEWORK_ADMIN_DIR . '/javascript/ssui.core.js',
345
				FRAMEWORK_DIR . '/javascript/GridField.js',
346
			)
347
		);
348
349
		if (Director::isDev()) Requirements::javascript(FRAMEWORK_ADMIN_DIR . '/javascript/leaktools.js');
350
351
		$leftAndMainIncludes = array_unique(array_merge(
352
			array(
353
				FRAMEWORK_ADMIN_DIR . '/javascript/LeftAndMain.Layout.js',
354
				FRAMEWORK_ADMIN_DIR . '/javascript/LeftAndMain.js',
355
				FRAMEWORK_ADMIN_DIR . '/javascript/LeftAndMain.ActionTabSet.js',
356
				FRAMEWORK_ADMIN_DIR . '/javascript/LeftAndMain.Panel.js',
357
				FRAMEWORK_ADMIN_DIR . '/javascript/LeftAndMain.Tree.js',
358
				FRAMEWORK_ADMIN_DIR . '/javascript/LeftAndMain.Content.js',
359
				FRAMEWORK_ADMIN_DIR . '/javascript/LeftAndMain.EditForm.js',
360
				FRAMEWORK_ADMIN_DIR . '/javascript/LeftAndMain.Menu.js',
361
				FRAMEWORK_ADMIN_DIR . '/javascript/LeftAndMain.Preview.js',
362
				FRAMEWORK_ADMIN_DIR . '/javascript/LeftAndMain.BatchActions.js',
363
				FRAMEWORK_ADMIN_DIR . '/javascript/LeftAndMain.FieldHelp.js',
364
				FRAMEWORK_ADMIN_DIR . '/javascript/LeftAndMain.FieldDescriptionToggle.js',
365
				FRAMEWORK_ADMIN_DIR . '/javascript/LeftAndMain.TreeDropdownField.js',
366
			),
367
			Requirements::add_i18n_javascript(FRAMEWORK_DIR . '/javascript/lang', true, true),
368
			Requirements::add_i18n_javascript(FRAMEWORK_ADMIN_DIR . '/javascript/lang', true, true)
369
		));
370
371
		if($this->config()->session_keepalive_ping) {
372
			$leftAndMainIncludes[] = FRAMEWORK_ADMIN_DIR . '/javascript/LeftAndMain.Ping.js';
373
		}
374
375
		Requirements::combine_files('leftandmain.js', $leftAndMainIncludes);
376
377
		// TODO Confuses jQuery.ondemand through document.write()
378
		if (Director::isDev()) {
379
			Requirements::javascript(THIRDPARTY_DIR . '/jquery-entwine/src/jquery.entwine.inspector.js');
380
		}
381
382
		Requirements::css(FRAMEWORK_ADMIN_DIR . '/thirdparty/jquery-notice/jquery.notice.css');
383
		Requirements::css(THIRDPARTY_DIR . '/jquery-ui-themes/smoothness/jquery-ui.css');
384
		Requirements::css(FRAMEWORK_ADMIN_DIR .'/thirdparty/chosen/chosen/chosen.css');
385
		Requirements::css(THIRDPARTY_DIR . '/jstree/themes/apple/style.css');
386
		Requirements::css(FRAMEWORK_DIR . '/css/TreeDropdownField.css');
387
		Requirements::css(FRAMEWORK_ADMIN_DIR . '/css/screen.css');
388
		Requirements::css(FRAMEWORK_DIR . '/css/GridField.css');
389
390
		// Browser-specific requirements
391
		$ie = isset($_SERVER['HTTP_USER_AGENT']) ? strpos($_SERVER['HTTP_USER_AGENT'], 'MSIE') : false;
392
		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...
393
			$version = substr($_SERVER['HTTP_USER_AGENT'], $ie + 5, 3);
394
395
			if($version == 7) {
396
				Requirements::css(FRAMEWORK_ADMIN_DIR . '/css/ie7.css');
397
			} else if($version == 8) {
398
				Requirements::css(FRAMEWORK_ADMIN_DIR . '/css/ie8.css');
399
			}
400
		}
401
402
		// Custom requirements
403
		$extraJs = $this->stat('extra_requirements_javascript');
404
405
		if($extraJs) {
406
			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...
407
				if(is_numeric($file)) {
408
					$file = $config;
409
				}
410
411
				Requirements::javascript($file);
412
			}
413
		}
414
415
		$extraCss = $this->stat('extra_requirements_css');
416
417 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...
418
			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...
419
				if(is_numeric($file)) {
420
					$file = $config;
421
					$config = array();
422
				}
423
424
				Requirements::css($file, isset($config['media']) ? $config['media'] : null);
425
			}
426
		}
427
428
		$extraThemedCss = $this->stat('extra_requirements_themedCss');
429
430 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...
431
			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...
432
				if(is_numeric($file)) {
433
					$file = $config;
434
					$config = array();
435
				}
436
437
				Requirements::themedCSS($file, isset($config['media']) ? $config['media'] : null);
438
			}
439
		}
440
441
		$dummy = null;
442
		$this->extend('init', $dummy);
443
444
		// The user's theme shouldn't affect the CMS, if, for example, they have
445
		// replaced TableListField.ss or Form.ss.
446
		Config::inst()->update('SSViewer', 'theme_enabled', false);
447
448
		//set the reading mode for the admin to stage
449
		Versioned::reading_stage('Stage');
450
	}
451
452
	public function handleRequest(SS_HTTPRequest $request, DataModel $model = null) {
453
		try {
454
			$response = parent::handleRequest($request, $model);
0 ignored issues
show
Bug introduced by
It seems like $model defined by parameter $model on line 452 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...
455
		} catch(ValidationException $e) {
456
			// Nicer presentation of model-level validation errors
457
			$msgs = _t('LeftAndMain.ValidationError', 'Validation error') . ': '
458
				. $e->getMessage();
459
			$e = new SS_HTTPResponse_Exception($msgs, 403);
460
			$errorResponse = $e->getResponse();
461
			$errorResponse->addHeader('Content-Type', 'text/plain');
462
			$errorResponse->addHeader('X-Status', rawurlencode($msgs));
463
			$e->setResponse($errorResponse);
464
			throw $e;
465
		}
466
467
		$title = $this->Title();
468
		if(!$response->getHeader('X-Controller')) $response->addHeader('X-Controller', $this->class);
469
		if(!$response->getHeader('X-Title')) $response->addHeader('X-Title', urlencode($title));
470
471
		// Prevent clickjacking, see https://developer.mozilla.org/en-US/docs/HTTP/X-Frame-Options
472
		$originalResponse = $this->getResponse();
473
		$originalResponse->addHeader('X-Frame-Options', 'SAMEORIGIN');
474
		$originalResponse->addHeader('Vary', 'X-Requested-With');
475
476
		return $response;
477
	}
478
479
	/**
480
	 * Overloaded redirection logic to trigger a fake redirect on ajax requests.
481
	 * While this violates HTTP principles, its the only way to work around the
482
	 * fact that browsers handle HTTP redirects opaquely, no intervention via JS is possible.
483
	 * In isolation, that's not a problem - but combined with history.pushState()
484
	 * it means we would request the same redirection URL twice if we want to update the URL as well.
485
	 * See LeftAndMain.js for the required jQuery ajaxComplete handlers.
486
	 */
487
	public function redirect($url, $code=302) {
488
		if($this->getRequest()->isAjax()) {
489
			$response = $this->getResponse();
490
			$response->addHeader('X-ControllerURL', $url);
491
			if($this->getRequest()->getHeader('X-Pjax') && !$response->getHeader('X-Pjax')) {
492
				$response->addHeader('X-Pjax', $this->getRequest()->getHeader('X-Pjax'));
493
			}
494
			$newResponse = new LeftAndMain_HTTPResponse(
495
				$response->getBody(),
496
				$response->getStatusCode(),
497
				$response->getStatusDescription()
498
			);
499
			foreach($response->getHeaders() as $k => $v) {
500
				$newResponse->addHeader($k, $v);
501
			}
502
			$newResponse->setIsFinished(true);
503
			$this->setResponse($newResponse);
504
			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...
505
		} else {
506
			parent::redirect($url, $code);
507
		}
508
	}
509
510
	public function index($request) {
511
		return $this->getResponseNegotiator()->respond($request);
512
	}
513
514
	/**
515
	 * If this is set to true, the "switchView" context in the
516
	 * template is shown, with links to the staging and publish site.
517
	 *
518
	 * @return boolean
519
	 */
520
	public function ShowSwitchView() {
521
		return false;
522
	}
523
524
525
	//------------------------------------------------------------------------------------------//
526
	// Main controllers
527
528
	/**
529
	 * You should implement a Link() function in your subclass of LeftAndMain,
530
	 * to point to the URL of that particular controller.
531
	 *
532
	 * @return string
533
	 */
534
	public function Link($action = null) {
535
		// Handle missing url_segments
536
		if($this->config()->url_segment) {
537
			$segment = $this->config()->get('url_segment', Config::FIRST_SET);
538
		} else {
539
			$segment = $this->class;
540
		};
541
542
		$link = Controller::join_links(
543
			$this->stat('url_base', true),
544
			$segment,
545
			'/', // trailing slash needed if $action is null!
546
			"$action"
547
		);
548
		$this->extend('updateLink', $link);
549
		return $link;
550
	}
551
552
	/**
553
	 * Returns the menu title for the given LeftAndMain subclass.
554
	 * Implemented static so that we can get this value without instantiating an object.
555
	 * Menu title is *not* internationalised.
556
	 */
557
	public static function menu_title_for_class($class) {
558
		$title = Config::inst()->get($class, 'menu_title', Config::FIRST_SET);
559
		if(!$title) $title = preg_replace('/Admin$/', '', $class);
560
		return $title;
561
	}
562
563
	/**
564
	 * Return styling for the menu icon, if a custom icon is set for this class
565
	 *
566
	 * Example: static $menu-icon = '/path/to/image/';
567
	 * @param string $class
568
	 * @return string
569
	 */
570
	public static function menu_icon_for_class($class) {
571
		$icon = Config::inst()->get($class, 'menu_icon', Config::FIRST_SET);
572
		if (!empty($icon)) {
573
			$class = strtolower(Convert::raw2htmlname(str_replace('\\', '-', $class)));
574
			return ".icon.icon-16.icon-{$class} { background-image: url('{$icon}'); } ";
575
		}
576
		return '';
577
	}
578
579
	public function show($request) {
580
		// TODO Necessary for TableListField URLs to work properly
581
		if($request->param('ID')) $this->setCurrentPageID($request->param('ID'));
582
		return $this->getResponseNegotiator()->respond($request);
583
	}
584
585
	/**
586
	 * Caution: Volatile API.
587
	 *
588
	 * @return PjaxResponseNegotiator
589
	 */
590
	public function getResponseNegotiator() {
591
		if(!$this->responseNegotiator) {
592
			$controller = $this;
593
			$this->responseNegotiator = new PjaxResponseNegotiator(
594
				array(
595
					'CurrentForm' => function() use(&$controller) {
596
						return $controller->getEditForm()->forTemplate();
0 ignored issues
show
Bug introduced by
The method forTemplate does only exist in CMSForm, but not in SS_HTTPResponse.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

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

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...
1278
			$form->addExtraClass('cms-edit-form');
1279
			$form->loadDataFrom($record);
1280
			$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...
1281
			$form->setAttribute('data-pjax-fragment', 'CurrentForm');
1282
1283
			// Announce the capability so the frontend can decide whether to allow preview or not.
1284
			if(in_array('CMSPreviewable', class_implements($record))) {
1285
				$form->addExtraClass('cms-previewable');
1286
			}
1287
1288
			// Set this if you want to split up tabs into a separate header row
1289
			// if($form->Fields()->hasTabset()) {
1290
			// 	$form->Fields()->findOrMakeTab('Root')->setTemplate('CMSTabSet');
1291
			// }
1292
1293
			// Add a default or custom validator.
1294
			// @todo Currently the default Validator.js implementation
1295
			//  adds javascript to the document body, meaning it won't
1296
			//  be included properly if the associated fields are loaded
1297
			//  through ajax. This means only serverside validation
1298
			//  will kick in for pages+validation loaded through ajax.
1299
			//  This will be solved by using less obtrusive javascript validation
1300
			//  in the future, see http://open.silverstripe.com/ticket/2915 and
1301
			//  http://open.silverstripe.com/ticket/3386
1302
			if($record->hasMethod('getCMSValidator')) {
1303
				$validator = $record->getCMSValidator();
1304
				// The clientside (mainly LeftAndMain*.js) rely on ajax responses
1305
				// which can be evaluated as javascript, hence we need
1306
				// to override any global changes to the validation handler.
1307
				if($validator != NULL){
1308
					$form->setValidator($validator);
1309
				}
1310
			} else {
1311
				$form->unsetValidator();
1312
			}
1313
1314
			if($record->hasMethod('canEdit') && !$record->canEdit()) {
1315
				$readonlyFields = $form->Fields()->makeReadonly();
1316
				$form->setFields($readonlyFields);
1317
			}
1318
		} else {
1319
			$form = $this->EmptyForm();
1320
		}
1321
1322
		return $form;
1323
	}
1324
1325
	/**
1326
	 * Returns a placeholder form, used by {@link getEditForm()} if no record is selected.
1327
	 * Our javascript logic always requires a form to be present in the CMS interface.
1328
	 *
1329
	 * @return Form
1330
	 */
1331
	public function EmptyForm() {
1332
		$form = CMSForm::create(
1333
			$this,
1334
			"EditForm",
1335
			new FieldList(
1336
				// new HeaderField(
1337
				// 	'WelcomeHeader',
1338
				// 	$this->getApplicationName()
1339
				// ),
1340
				// new LiteralField(
1341
				// 	'WelcomeText',
1342
				// 	sprintf('<p id="WelcomeMessage">%s %s. %s</p>',
1343
				// 		_t('LeftAndMain_right_ss.WELCOMETO','Welcome to'),
1344
				// 		$this->getApplicationName(),
1345
				// 		_t('CHOOSEPAGE','Please choose an item from the left.')
1346
				// 	)
1347
				// )
1348
			),
1349
			new FieldList()
1350
		)->setHTMLID('Form_EditForm');
1351
		$form->setResponseNegotiator($this->getResponseNegotiator());
0 ignored issues
show
Documentation introduced by
$this->getResponseNegotiator() is of type object<PjaxResponseNegotiator>, but the function expects a object<ResponseNegotiator>.

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...
1352
		$form->unsetValidator();
1353
		$form->addExtraClass('cms-edit-form');
1354
		$form->addExtraClass('root-form');
1355
		$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...
1356
		$form->setAttribute('data-pjax-fragment', 'CurrentForm');
1357
1358
		return $form;
1359
	}
1360
1361
	/**
1362
	 * Return the CMS's HTML-editor toolbar
1363
	 */
1364
	public function EditorToolbar() {
1365
		return HtmlEditorField_Toolbar::create($this, "EditorToolbar");
1366
	}
1367
1368
	/**
1369
	 * Renders a panel containing tools which apply to all displayed
1370
	 * "content" (mostly through {@link EditForm()}), for example a tree navigation or a filter panel.
1371
	 * Auto-detects applicable templates by naming convention: "<controller classname>_Tools.ss",
1372
	 * and takes the most specific template (see {@link getTemplatesWithSuffix()}).
1373
	 * To explicitly disable the panel in the subclass, simply create a more specific, empty template.
1374
	 *
1375
	 * @return String HTML
1376
	 */
1377 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...
1378
		$templates = $this->getTemplatesWithSuffix('_Tools');
1379
		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...
1380
			$viewer = new SSViewer($templates);
1381
			return $viewer->process($this);
1382
		} else {
1383
			return false;
1384
		}
1385
	}
1386
1387
	/**
1388
	 * Renders a panel containing tools which apply to the currently displayed edit form.
1389
	 * The main difference to {@link Tools()} is that the panel is displayed within
1390
	 * the element structure of the form panel (rendered through {@link EditForm}).
1391
	 * This means the panel will be loaded alongside new forms, and refreshed upon save,
1392
	 * which can mean a performance hit, depending on how complex your panel logic gets.
1393
	 * Any form fields contained in the returned markup will also be submitted with the main form,
1394
	 * which might be desired depending on the implementation details.
1395
	 *
1396
	 * @return String HTML
1397
	 */
1398 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...
1399
		$templates = $this->getTemplatesWithSuffix('_EditFormTools');
1400
		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...
1401
			$viewer = new SSViewer($templates);
1402
			return $viewer->process($this);
1403
		} else {
1404
			return false;
1405
		}
1406
	}
1407
1408
	/**
1409
	 * Batch Actions Handler
1410
	 */
1411
	public function batchactions() {
1412
		return new CMSBatchActionHandler($this, 'batchactions', $this->stat('tree_class'));
1413
	}
1414
1415
	/**
1416
	 * @return Form
1417
	 */
1418
	public function BatchActionsForm() {
1419
		$actions = $this->batchactions()->batchActionList();
1420
		$actionsMap = array('-1' => _t('LeftAndMain.DropdownBatchActionsDefault', 'Choose an action...')); // Placeholder action
1421
		foreach($actions as $action) {
1422
			$actionsMap[$action->Link] = $action->Title;
1423
		}
1424
1425
		$form = new Form(
1426
			$this,
1427
			'BatchActionsForm',
1428
			new FieldList(
1429
				new HiddenField('csvIDs'),
1430
				DropdownField::create(
1431
					'Action',
1432
					false,
1433
					$actionsMap
1434
				)
1435
					->setAttribute('autocomplete', 'off')
1436
					->setAttribute('data-placeholder', _t('LeftAndMain.DropdownBatchActionsDefault', 'Choose an action...'))
1437
			),
1438
			new FieldList(
1439
				// TODO i18n
1440
				new FormAction('submit', _t('Form.SubmitBtnLabel', "Go"))
1441
			)
1442
		);
1443
		$form->addExtraClass('cms-batch-actions nostyle');
1444
		$form->unsetValidator();
1445
1446
		$this->extend('updateBatchActionsForm', $form);
1447
		return $form;
1448
	}
1449
1450
	public function printable() {
1451
		$form = $this->getEditForm($this->currentPageID());
1452
		if(!$form) return false;
1453
1454
		$form->transform(new PrintableTransformation());
0 ignored issues
show
Bug introduced by
The method transform does only exist in CMSForm, but not in SS_HTTPResponse.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
1455
		$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...
Bug introduced by
The method setActions does only exist in CMSForm, but not in SS_HTTPResponse.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
1456
1457
		Requirements::clear();
1458
		Requirements::css(FRAMEWORK_ADMIN_DIR . '/css/LeftAndMain_printable.css');
1459
		return array(
1460
			"PrintForm" => $form
1461
		);
1462
	}
1463
1464
	/**
1465
	 * Used for preview controls, mainly links which switch between different states of the page.
1466
	 *
1467
	 * @return ArrayData
1468
	 */
1469
	public function getSilverStripeNavigator() {
1470
		$page = $this->currentPage();
1471
		if($page) {
1472
			$navigator = new SilverStripeNavigator($page);
1473
			return $navigator->renderWith($this->getTemplatesWithSuffix('_SilverStripeNavigator'));
1474
		} else {
1475
			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...
1476
		}
1477
	}
1478
1479
	/**
1480
	 * Identifier for the currently shown record,
1481
	 * in most cases a database ID. Inspects the following
1482
	 * sources (in this order):
1483
	 * - GET/POST parameter named 'ID'
1484
	 * - URL parameter named 'ID'
1485
	 * - Session value namespaced by classname, e.g. "CMSMain.currentPage"
1486
	 *
1487
	 * @return int
1488
	 */
1489
	public function currentPageID() {
1490
		if($this->getRequest()->requestVar('ID') && is_numeric($this->getRequest()->requestVar('ID')))	{
1491
			return $this->getRequest()->requestVar('ID');
1492
		} elseif (isset($this->urlParams['ID']) && is_numeric($this->urlParams['ID'])) {
1493
			return $this->urlParams['ID'];
1494
		} elseif(Session::get($this->sessionNamespace() . ".currentPage")) {
1495
			return Session::get($this->sessionNamespace() . ".currentPage");
1496
		} else {
1497
			return null;
1498
		}
1499
	}
1500
1501
	/**
1502
	 * Forces the current page to be set in session,
1503
	 * which can be retrieved later through {@link currentPageID()}.
1504
	 * Keep in mind that setting an ID through GET/POST or
1505
	 * as a URL parameter will overrule this value.
1506
	 *
1507
	 * @param int $id
1508
	 */
1509
	public function setCurrentPageID($id) {
1510
		$id = (int)$id;
1511
		Session::set($this->sessionNamespace() . ".currentPage", $id);
1512
	}
1513
1514
	/**
1515
	 * Uses {@link getRecord()} and {@link currentPageID()}
1516
	 * to get the currently selected record.
1517
	 *
1518
	 * @return DataObject
1519
	 */
1520
	public function currentPage() {
1521
		return $this->getRecord($this->currentPageID());
1522
	}
1523
1524
	/**
1525
	 * Compares a given record to the currently selected one (if any).
1526
	 * Used for marking the current tree node.
1527
	 *
1528
	 * @return boolean
1529
	 */
1530
	public function isCurrentPage(DataObject $record) {
1531
		return ($record->ID == $this->currentPageID());
1532
	}
1533
1534
	/**
1535
	 * @return String
1536
	 */
1537
	protected function sessionNamespace() {
1538
		$override = $this->stat('session_namespace');
1539
		return $override ? $override : $this->class;
1540
	}
1541
1542
	/**
1543
	 * URL to a previewable record which is shown through this controller.
1544
	 * The controller might not have any previewable content, in which case
1545
	 * this method returns FALSE.
1546
	 *
1547
	 * @return String|boolean
1548
	 */
1549
	public function LinkPreview() {
1550
		return false;
1551
	}
1552
1553
	/**
1554
	 * Return the version number of this application.
1555
	 * Uses the number in <mymodule>/silverstripe_version
1556
	 * (automatically replaced by build scripts).
1557
	 * If silverstripe_version is empty,
1558
	 * then attempts to get it from composer.lock
1559
	 *
1560
	 * @return string
1561
	 */
1562
	public function CMSVersion() {
1563
		$versions = array();
1564
		$modules = array(
1565
			'silverstripe/framework' => array(
1566
				'title' => 'Framework',
1567
				'versionFile' => FRAMEWORK_PATH . '/silverstripe_version',
1568
			)
1569
		);
1570
		if(defined('CMS_PATH')) {
1571
			$modules['silverstripe/cms'] = array(
1572
				'title' => 'CMS',
1573
				'versionFile' => CMS_PATH . '/silverstripe_version',
1574
			);
1575
		}
1576
1577
		// Tries to obtain version number from composer.lock if it exists
1578
		$composerLockPath = BASE_PATH . '/composer.lock';
1579
		if (file_exists($composerLockPath)) {
1580
			$cache = SS_Cache::factory('LeftAndMain_CMSVersion');
1581
			$cacheKey = filemtime($composerLockPath);
1582
			$versions = $cache->load($cacheKey);
1583
			if($versions) {
1584
				$versions = json_decode($versions, true);
1585
			} else {
1586
				$versions = array();
1587
			}
1588
			if(!$versions && $jsonData = file_get_contents($composerLockPath)) {
1589
				$lockData = json_decode($jsonData);
1590
				if($lockData && isset($lockData->packages)) {
1591
					foreach ($lockData->packages as $package) {
1592
						if(
1593
							array_key_exists($package->name, $modules)
1594
							&& isset($package->version)
1595
						) {
1596
							$versions[$package->name] = $package->version;
1597
						}
1598
					}
1599
					$cache->save(json_encode($versions), $cacheKey);
1600
				}
1601
			}
1602
		}
1603
1604
		// Fall back to static version file
1605
		foreach($modules as $moduleName => $moduleSpec) {
1606
			if(!isset($versions[$moduleName])) {
1607
				if($staticVersion = file_get_contents($moduleSpec['versionFile'])) {
1608
					$versions[$moduleName] = $staticVersion;
1609
				} else {
1610
					$versions[$moduleName] = _t('LeftAndMain.VersionUnknown', 'Unknown');
1611
				}
1612
			}
1613
		}
1614
1615
		$out = array();
1616
		foreach($modules as $moduleName => $moduleSpec) {
1617
			$out[] = $modules[$moduleName]['title'] . ': ' . $versions[$moduleName];
1618
		}
1619
		return implode(', ', $out);
1620
	}
1621
1622
	/**
1623
	 * @return array
1624
	 */
1625
	public function SwitchView() {
1626
		if($page = $this->currentPage()) {
1627
			$nav = SilverStripeNavigator::get_for_record($page);
1628
			return $nav['items'];
1629
		}
1630
	}
1631
1632
	/**
1633
	 * @return SiteConfig
1634
	 */
1635
	public function SiteConfig() {
1636
		return (class_exists('SiteConfig')) ? SiteConfig::current_site_config() : null;
1637
	}
1638
1639
	/**
1640
	 * The href for the anchor on the Silverstripe logo.
1641
	 * Set by calling LeftAndMain::set_application_link()
1642
	 *
1643
	 * @config
1644
	 * @var String
1645
	 */
1646
	private static $application_link = '//www.silverstripe.org/';
1647
1648
	/**
1649
	 * Sets the href for the anchor on the Silverstripe logo in the menu
1650
	 *
1651
	 * @deprecated since version 4.0
1652
	 *
1653
	 * @param String $link
1654
	 */
1655
	public static function set_application_link($link) {
1656
		Deprecation::notice('4.0', 'Use the "LeftAndMain.application_link" config setting instead');
1657
		Config::inst()->update('LeftAndMain', 'application_link', $link);
1658
	}
1659
1660
	/**
1661
	 * @return String
1662
	 */
1663
	public function ApplicationLink() {
1664
		return $this->stat('application_link');
1665
	}
1666
1667
	/**
1668
	 * The application name. Customisable by calling
1669
	 * LeftAndMain::setApplicationName() - the first parameter.
1670
	 *
1671
	 * @config
1672
	 * @var String
1673
	 */
1674
	private static $application_name = 'SilverStripe';
1675
1676
	/**
1677
	 * @param String $name
1678
	 * @deprecated since version 4.0
1679
	 */
1680
	public static function setApplicationName($name) {
1681
		Deprecation::notice('4.0', 'Use the "LeftAndMain.application_name" config setting instead');
1682
		Config::inst()->update('LeftAndMain', 'application_name', $name);
1683
	}
1684
1685
	/**
1686
	 * Get the application name.
1687
	 *
1688
	 * @return string
1689
	 */
1690
	public function getApplicationName() {
1691
		return $this->stat('application_name');
1692
	}
1693
1694
	/**
1695
	 * @return string
1696
	 */
1697
	public function Title() {
1698
		$app = $this->getApplicationName();
1699
1700
		return ($section = $this->SectionTitle()) ? sprintf('%s - %s', $app, $section) : $app;
1701
	}
1702
1703
	/**
1704
	 * Return the title of the current section. Either this is pulled from
1705
	 * the current panel's menu_title or from the first active menu
1706
	 *
1707
	 * @return string
1708
	 */
1709
	public function SectionTitle() {
1710
		$class = get_class($this);
1711
		$defaultTitle = LeftAndMain::menu_title_for_class($class);
1712
		if($title = _t("{$class}.MENUTITLE", $defaultTitle)) return $title;
1713
1714
		foreach($this->MainMenu() as $menuItem) {
1715
			if($menuItem->LinkingMode != 'link') return $menuItem->Title;
1716
		}
1717
	}
1718
1719
	/**
1720
	 * Return the base directory of the tiny_mce codebase
1721
	 */
1722
	public function MceRoot() {
1723
		return MCE_ROOT;
1724
	}
1725
1726
	/**
1727
	 * Same as {@link ViewableData->CSSClasses()}, but with a changed name
1728
	 * to avoid problems when using {@link ViewableData->customise()}
1729
	 * (which always returns "ArrayData" from the $original object).
1730
	 *
1731
	 * @return String
1732
	 */
1733
	public function BaseCSSClasses() {
1734
		return $this->CSSClasses('Controller');
1735
	}
1736
1737
	/**
1738
	 * @return String
1739
	 */
1740
	public function Locale() {
1741
		return DBField::create_field('DBLocale', i18n::get_locale());
1742
	}
1743
1744
	public function providePermissions() {
1745
		$perms = array(
1746
			"CMS_ACCESS_LeftAndMain" => array(
1747
				'name' => _t('CMSMain.ACCESSALLINTERFACES', 'Access to all CMS sections'),
1748
				'category' => _t('Permission.CMS_ACCESS_CATEGORY', 'CMS Access'),
1749
				'help' => _t('CMSMain.ACCESSALLINTERFACESHELP', 'Overrules more specific access settings.'),
1750
				'sort' => -100
1751
			)
1752
		);
1753
1754
		// Add any custom ModelAdmin subclasses. Can't put this on ModelAdmin itself
1755
		// since its marked abstract, and needs to be singleton instanciated.
1756
		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...
1757
			if($class == 'ModelAdmin') continue;
1758
			if(ClassInfo::classImplements($class, 'TestOnly')) continue;
1759
1760
			$title = _t("{$class}.MENUTITLE", LeftAndMain::menu_title_for_class($class));
1761
			$perms["CMS_ACCESS_" . $class] = array(
1762
				'name' => _t(
1763
					'CMSMain.ACCESS',
1764
					"Access to '{title}' section",
1765
					"Item in permission selection identifying the admin section. Example: Access to 'Files & Images'",
1766
					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...
1767
				),
1768
				'category' => _t('Permission.CMS_ACCESS_CATEGORY', 'CMS Access')
1769
			);
1770
		}
1771
1772
		return $perms;
1773
	}
1774
1775
	/**
1776
	 * Register the given javascript file as required in the CMS.
1777
	 * Filenames should be relative to the base, eg, FRAMEWORK_DIR . '/javascript/loader.js'
1778
	 *
1779
	 * @deprecated since version 4.0
1780
	 */
1781
	public static function require_javascript($file) {
1782
		Deprecation::notice('4.0', 'Use "LeftAndMain.extra_requirements_javascript" config setting instead');
1783
		Config::inst()->update('LeftAndMain', 'extra_requirements_javascript', array($file => array()));
1784
	}
1785
1786
	/**
1787
	 * Register the given stylesheet file as required.
1788
	 * @deprecated since version 4.0
1789
	 *
1790
	 * @param $file String Filenames should be relative to the base, eg, THIRDPARTY_DIR . '/tree/tree.css'
1791
	 * @param $media String Comma-separated list of media-types (e.g. "screen,projector")
1792
	 * @see http://www.w3.org/TR/REC-CSS2/media.html
1793
	 */
1794
	public static function require_css($file, $media = null) {
1795
		Deprecation::notice('4.0', 'Use "LeftAndMain.extra_requirements_css" config setting instead');
1796
		Config::inst()->update('LeftAndMain', 'extra_requirements_css', array($file => array('media' => $media)));
1797
	}
1798
1799
	/**
1800
	 * Register the given "themeable stylesheet" as required.
1801
	 * Themeable stylesheets have globally unique names, just like templates and PHP files.
1802
	 * Because of this, they can be replaced by similarly named CSS files in the theme directory.
1803
	 *
1804
	 * @deprecated since version 4.0
1805
	 *
1806
	 * @param $name String The identifier of the file.  For example, css/MyFile.css would have the identifier "MyFile"
1807
	 * @param $media String Comma-separated list of media-types (e.g. "screen,projector")
1808
	 */
1809
	public static function require_themed_css($name, $media = null) {
1810
		Deprecation::notice('4.0', 'Use "LeftAndMain.extra_requirements_themedCss" config setting instead');
1811
		Config::inst()->update('LeftAndMain', 'extra_requirements_themedCss', array($name => array('media' => $media)));
1812
	}
1813
1814
}
1815
1816
/**
1817
 * @package cms
1818
 * @subpackage core
1819
 */
1820
class LeftAndMainMarkingFilter {
1821
1822
	/**
1823
	 * @var array Request params (unsanitized)
1824
	 */
1825
	protected $params = array();
1826
1827
	/**
1828
	 * @param array $params Request params (unsanitized)
1829
	 */
1830
	public function __construct($params = null) {
1831
		$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...
1832
		$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...
1833
		$parents = array();
1834
1835
		$q = $this->getQuery($params);
1836
		$res = $q->execute();
1837
		if (!$res) return;
1838
1839
		// And keep a record of parents we don't need to get parents
1840
		// of themselves, as well as IDs to mark
1841
		foreach($res as $row) {
1842
			if ($row['ParentID']) $parents[$row['ParentID']] = true;
1843
			$this->ids[$row['ID']] = true;
1844
		}
1845
1846
		// We need to recurse up the tree,
1847
		// finding ParentIDs for each ID until we run out of parents
1848
		while (!empty($parents)) {
1849
			$parentsClause = DB::placeholders($parents);
1850
			$res = DB::prepared_query(
1851
				"SELECT \"ParentID\", \"ID\" FROM \"SiteTree\" WHERE \"ID\" in ($parentsClause)",
1852
				array_keys($parents)
1853
			);
1854
			$parents = array();
1855
1856
			foreach($res as $row) {
1857
				if ($row['ParentID']) $parents[$row['ParentID']] = true;
1858
				$this->ids[$row['ID']] = true;
1859
				$this->expanded[$row['ID']] = true;
1860
			}
1861
		}
1862
	}
1863
1864
	protected function getQuery($params) {
1865
		$where = array();
1866
1867
		if(isset($params['ID'])) unset($params['ID']);
1868
		if($treeClass = static::config()->tree_class) foreach($params as $name => $val) {
1869
			// Partial string match against a variety of fields
1870
			if(!empty($val) && singleton($treeClass)->hasDatabaseField($name)) {
1871
				$predicate = sprintf('"%s" LIKE ?', $name);
1872
				$where[$predicate] = "%$val%";
1873
			}
1874
		}
1875
1876
		return new SQLQuery(
0 ignored issues
show
Deprecated Code introduced by
The class SQLQuery has been deprecated with message: since version 4.0

This class, trait or interface has been deprecated. The supplier of the file has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the type will be removed from the class and what other constant to use instead.

Loading history...
1877
			array("ParentID", "ID"),
0 ignored issues
show
Documentation introduced by
array('ParentID', 'ID') is of type array<integer,string,{"0":"string","1":"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...
1878
			'SiteTree',
0 ignored issues
show
Documentation introduced by
'SiteTree' is of type string, but the function expects a array.

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...
1879
			$where
1880
		);
1881
	}
1882
1883
	public function mark($node) {
1884
		$id = $node->ID;
1885
		if(array_key_exists((int) $id, $this->expanded)) $node->markOpened();
1886
		return array_key_exists((int) $id, $this->ids) ? $this->ids[$id] : false;
1887
	}
1888
}
1889
1890
/**
1891
 * Allow overriding finished state for faux redirects.
1892
 *
1893
 * @package framework
1894
 * @subpackage admin
1895
 */
1896
class LeftAndMain_HTTPResponse extends SS_HTTPResponse {
1897
1898
	protected $isFinished = false;
1899
1900
	public function isFinished() {
1901
		return (parent::isFinished() || $this->isFinished);
1902
	}
1903
1904
	public function setIsFinished($bool) {
1905
		$this->isFinished = $bool;
1906
	}
1907
1908
}
1909
1910
/**
1911
 * Wrapper around objects being displayed in a tree.
1912
 * Caution: Volatile API.
1913
 *
1914
 * @todo Implement recursive tree node rendering.
1915
 *
1916
 * @package framework
1917
 * @subpackage admin
1918
 */
1919
class LeftAndMain_TreeNode extends ViewableData {
1920
1921
	/**
1922
	 * Object represented by this node
1923
	 *
1924
	 * @var Object
1925
	 */
1926
	protected $obj;
1927
1928
	/**
1929
	 * Edit link to the current record in the CMS
1930
	 *
1931
	 * @var string
1932
	 */
1933
	protected $link;
1934
1935
	/**
1936
	 * True if this is the currently selected node in the tree
1937
	 *
1938
	 * @var bool
1939
	 */
1940
	protected $isCurrent;
1941
1942
	/**
1943
	 * Name of method to count the number of children
1944
	 *
1945
	 * @var string
1946
	 */
1947
	protected $numChildrenMethod;
1948
1949
1950
	/**
1951
	 *
1952
	 * @var LeftAndMain_SearchFilter
1953
	 */
1954
	protected $filter;
1955
1956
	/**
1957
	 * @param Object $obj
1958
	 * @param string $link
1959
	 * @param bool $isCurrent
1960
	 * @param string $numChildrenMethod
1961
	 * @param LeftAndMain_SearchFilter $filter
1962
	 */
1963
	public function __construct($obj, $link = null, $isCurrent = false,
1964
		$numChildrenMethod = 'numChildren', $filter = null
1965
	) {
1966
		parent::__construct();
1967
		$this->obj = $obj;
1968
		$this->link = $link;
1969
		$this->isCurrent = $isCurrent;
1970
		$this->numChildrenMethod = $numChildrenMethod;
1971
		$this->filter = $filter;
1972
	}
1973
1974
	/**
1975
	 * Returns template, for further processing by {@link Hierarchy->getChildrenAsUL()}.
1976
	 * Does not include closing tag to allow this method to inject its own children.
1977
	 *
1978
	 * @todo Remove hardcoded assumptions around returning an <li>, by implementing recursive tree node rendering
1979
	 *
1980
	 * @return String
1981
	 */
1982
	public function forTemplate() {
1983
		$obj = $this->obj;
1984
		return "<li id=\"record-$obj->ID\" data-id=\"$obj->ID\" data-pagetype=\"$obj->ClassName\" class=\""
1985
			. $this->getClasses() . "\">" . "<ins class=\"jstree-icon\">&nbsp;</ins>"
1986
			. "<a href=\"" . $this->getLink() . "\" title=\"("
1987
			. trim(_t('LeftAndMain.PAGETYPE','Page type'), " :") // account for inconsistencies in translations
1988
			. ": " . $obj->i18n_singular_name() . ") $obj->Title\" ><ins class=\"jstree-icon\">&nbsp;</ins><span class=\"text\">" . ($obj->TreeTitle)
1989
			. "</span></a>";
1990
	}
1991
1992
	/**
1993
	 * Determine the CSS classes to apply to this node
1994
	 *
1995
	 * @return string
1996
	 */
1997
	public function getClasses() {
1998
		// Get classes from object
1999
		$classes = $this->obj->CMSTreeClasses($this->numChildrenMethod);
2000
		if($this->isCurrent) {
2001
			$classes .= ' current';
2002
		}
2003
		// Get status flag classes
2004
		$flags = $this->obj->hasMethod('getStatusFlags')
2005
			? $this->obj->getStatusFlags()
2006
			: false;
2007
		if ($flags) {
2008
			$statuses = array_keys($flags);
2009
			foreach ($statuses as $s) {
2010
				$classes .= ' status-' . $s;
2011
			}
2012
		}
2013
		// Get additional filter classes
2014
		if($this->filter && ($filterClasses = $this->filter->getPageClasses($this->obj))) {
2015
			if(is_array($filterClasses)) {
2016
				$filterClasses = implode(' ' . $filterClasses);
2017
			}
2018
			$classes .= ' ' . $filterClasses;
2019
		}
2020
		return $classes;
2021
	}
2022
2023
	public function getObj() {
2024
		return $this->obj;
2025
	}
2026
2027
	public function setObj($obj) {
2028
		$this->obj = $obj;
2029
		return $this;
2030
	}
2031
2032
	public function getLink() {
2033
		return $this->link;
2034
	}
2035
2036
	public function setLink($link) {
2037
		$this->link = $link;
2038
		return $this;
2039
	}
2040
2041
	public function getIsCurrent() {
2042
		return $this->isCurrent;
2043
	}
2044
2045
	public function setIsCurrent($bool) {
2046
		$this->isCurrent = $bool;
2047
		return $this;
2048
	}
2049
2050
}
2051
2052
/**
2053
 * Abstract interface for a class which may be used to filter the results displayed
2054
 * in a nested tree
2055
 */
2056
interface LeftAndMain_SearchFilter {
2057
2058
	/**
2059
	 * Method on {@link Hierarchy} objects which is used to traverse into children relationships.
2060
	 *
2061
	 * @return string
2062
	 */
2063
	public function getChildrenMethod();
2064
2065
	/**
2066
	 * Method on {@link Hierarchy} objects which is used find the number of children for a parent page
2067
	 *
2068
	 * @return string
2069
	 */
2070
	public function getNumChildrenMethod();
2071
2072
2073
	/**
2074
	 * Returns TRUE if the given page should be included in the tree.
2075
	 * Caution: Does NOT check view permissions on the page.
2076
	 *
2077
	 * @param DataObject $page
2078
	 * @return bool
2079
	 */
2080
	public function isPageIncluded($page);
2081
2082
	/**
2083
	 * Given a page, determine any additional CSS classes to apply to the tree node
2084
	 *
2085
	 * @param DataObject $page
2086
	 * @return array|string
2087
	 */
2088
	public function getPageClasses($page);
2089
}
2090