Completed
Push — 3.4 ( e1ee92...609b0a )
by Damian
07:43
created

LeftAndMain_TreeNode::setLink()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 3
nc 1
nop 1
dl 0
loc 4
rs 10
c 0
b 0
f 0
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.4';
83
84
	/**
85
	 * @var array
86
	 */
87
	private static $allowed_actions = array(
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() {
0 ignored issues
show
Coding Style introduced by
init uses the super-global variable $_SERVER which is generally not recommended.

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

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

// Better
class Router
{
    private $host;

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

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

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

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
Coding Style introduced by
init uses the super-global variable $_REQUEST which is generally not recommended.

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

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

// Better
class Router
{
    private $host;

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

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

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

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
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
		HtmlEditorConfig::require_js();
383
384
		Requirements::css(FRAMEWORK_ADMIN_DIR . '/thirdparty/jquery-notice/jquery.notice.css');
385
		Requirements::css(THIRDPARTY_DIR . '/jquery-ui-themes/smoothness/jquery-ui.css');
386
		Requirements::css(FRAMEWORK_ADMIN_DIR .'/thirdparty/chosen/chosen/chosen.css');
387
		Requirements::css(THIRDPARTY_DIR . '/jstree/themes/apple/style.css');
388
		Requirements::css(FRAMEWORK_DIR . '/css/TreeDropdownField.css');
389
		Requirements::css(FRAMEWORK_ADMIN_DIR . '/css/screen.css');
390
		Requirements::css(FRAMEWORK_DIR . '/css/GridField.css');
391
392
		// Browser-specific requirements
393
		$ie = isset($_SERVER['HTTP_USER_AGENT']) ? strpos($_SERVER['HTTP_USER_AGENT'], 'MSIE') : false;
394
		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...
395
			$version = substr($_SERVER['HTTP_USER_AGENT'], $ie + 5, 3);
396
397
			if($version == 7) {
398
				Requirements::css(FRAMEWORK_ADMIN_DIR . '/css/ie7.css');
399
			} else if($version == 8) {
400
				Requirements::css(FRAMEWORK_ADMIN_DIR . '/css/ie8.css');
401
			}
402
		}
403
404
		// Custom requirements
405
		$extraJs = $this->stat('extra_requirements_javascript');
406
407
		if($extraJs) {
408
			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...
409
				if(is_numeric($file)) {
410
					$file = $config;
411
				}
412
413
				Requirements::javascript($file);
414
			}
415
		}
416
417
		$extraCss = $this->stat('extra_requirements_css');
418
419
		if($extraCss) {
420
			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...
421
				if(is_numeric($file)) {
422
					$file = $config;
423
					$config = array();
424
				}
425
426
				Requirements::css($file, isset($config['media']) ? $config['media'] : null);
427
			}
428
		}
429
430
		$extraThemedCss = $this->stat('extra_requirements_themedCss');
431
432
		if($extraThemedCss) {
433
			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...
434
				if(is_numeric($file)) {
435
					$file = $config;
436
					$config = array();
437
				}
438
439
				Requirements::themedCSS($file, isset($config['media']) ? $config['media'] : null);
440
			}
441
		}
442
443
		$dummy = null;
444
		$this->extend('init', $dummy);
445
446
		// The user's theme shouldn't affect the CMS, if, for example, they have
447
		// replaced TableListField.ss or Form.ss.
448
		Config::inst()->update('SSViewer', 'theme_enabled', false);
449
450
		//set the reading mode for the admin to stage
451
		Versioned::reading_stage('Stage');
452
	}
453
454
	public function handleRequest(SS_HTTPRequest $request, DataModel $model = null) {
455
		try {
456
			$response = parent::handleRequest($request, $model);
0 ignored issues
show
Bug introduced by
It seems like $model defined by parameter $model on line 454 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...
457
		} catch(ValidationException $e) {
458
			// Nicer presentation of model-level validation errors
459
			$msgs = _t('LeftAndMain.ValidationError', 'Validation error') . ': '
460
				. $e->getMessage();
461
			$e = new SS_HTTPResponse_Exception($msgs, 403);
462
			$errorResponse = $e->getResponse();
463
			$errorResponse->addHeader('Content-Type', 'text/plain');
464
			$errorResponse->addHeader('X-Status', rawurlencode($msgs));
465
			$e->setResponse($errorResponse);
466
			throw $e;
467
		}
468
469
		$title = $this->Title();
470
		if(!$response->getHeader('X-Controller')) $response->addHeader('X-Controller', $this->class);
471
		if(!$response->getHeader('X-Title')) $response->addHeader('X-Title', urlencode($title));
472
473
		// Prevent clickjacking, see https://developer.mozilla.org/en-US/docs/HTTP/X-Frame-Options
474
		$originalResponse = $this->getResponse();
475
		$originalResponse->addHeader('X-Frame-Options', 'SAMEORIGIN');
476
		$originalResponse->addHeader('Vary', 'X-Requested-With');
477
478
		return $response;
479
	}
480
481
	/**
482
	 * Overloaded redirection logic to trigger a fake redirect on ajax requests.
483
	 * While this violates HTTP principles, its the only way to work around the
484
	 * fact that browsers handle HTTP redirects opaquely, no intervention via JS is possible.
485
	 * In isolation, that's not a problem - but combined with history.pushState()
486
	 * it means we would request the same redirection URL twice if we want to update the URL as well.
487
	 * See LeftAndMain.js for the required jQuery ajaxComplete handlers.
488
	 */
489
	public function redirect($url, $code=302) {
490
		if($this->getRequest()->isAjax()) {
491
			$response = $this->getResponse();
492
			$response->addHeader('X-ControllerURL', $url);
493
			if($this->getRequest()->getHeader('X-Pjax') && !$response->getHeader('X-Pjax')) {
494
				$response->addHeader('X-Pjax', $this->getRequest()->getHeader('X-Pjax'));
495
			}
496
			$newResponse = new LeftAndMain_HTTPResponse(
497
				$response->getBody(),
498
				$response->getStatusCode(),
499
				$response->getStatusDescription()
500
			);
501
			foreach($response->getHeaders() as $k => $v) {
502
				$newResponse->addHeader($k, $v);
503
			}
504
			$newResponse->setIsFinished(true);
505
			$this->setResponse($newResponse);
506
			return ''; // Actual response will be re-requested by client
507
		} else {
508
			parent::redirect($url, $code);
509
		}
510
	}
511
512
	public function index($request) {
513
		return $this->getResponseNegotiator()->respond($request);
514
	}
515
516
	/**
517
	 * If this is set to true, the "switchView" context in the
518
	 * template is shown, with links to the staging and publish site.
519
	 *
520
	 * @return boolean
521
	 */
522
	public function ShowSwitchView() {
523
		return false;
524
	}
525
526
527
	//------------------------------------------------------------------------------------------//
528
	// Main controllers
529
530
	/**
531
	 * You should implement a Link() function in your subclass of LeftAndMain,
532
	 * to point to the URL of that particular controller.
533
	 *
534
	 * @return string
535
	 */
536
	public function Link($action = null) {
537
		// Handle missing url_segments
538
		if($this->config()->url_segment) {
539
			$segment = $this->config()->get('url_segment', Config::FIRST_SET);
540
		} else {
541
			$segment = $this->class;
542
		};
543
544
		$link = Controller::join_links(
545
			$this->stat('url_base', true),
546
			$segment,
547
			'/', // trailing slash needed if $action is null!
548
			"$action"
549
		);
550
		$this->extend('updateLink', $link);
551
		return $link;
552
	}
553
554
	/**
555
	 * Returns the menu title for the given LeftAndMain subclass.
556
	 * Implemented static so that we can get this value without instantiating an object.
557
	 * Menu title is *not* internationalised.
558
	 */
559
	public static function menu_title_for_class($class) {
560
		$title = Config::inst()->get($class, 'menu_title', Config::FIRST_SET);
561
		if(!$title) $title = preg_replace('/Admin$/', '', $class);
562
		return $title;
563
	}
564
565
	/**
566
	 * Return styling for the menu icon, if a custom icon is set for this class
567
	 *
568
	 * Example: static $menu-icon = '/path/to/image/';
569
	 * @param string $class
570
	 * @return string
571
	 */
572
	public static function menu_icon_for_class($class) {
573
		$icon = Config::inst()->get($class, 'menu_icon', Config::FIRST_SET);
574
		if (!empty($icon)) {
575
			$class = strtolower(Convert::raw2htmlname(str_replace('\\', '-', $class)));
576
			return ".icon.icon-16.icon-{$class} { background-image: url('{$icon}'); } ";
577
		}
578
		return '';
579
	}
580
581
	public function show($request) {
582
		// TODO Necessary for TableListField URLs to work properly
583
		if($request->param('ID')) $this->setCurrentPageID($request->param('ID'));
584
		return $this->getResponseNegotiator()->respond($request);
585
	}
586
587
	/**
588
	 * Caution: Volatile API.
589
	 *
590
	 * @return PjaxResponseNegotiator
591
	 */
592
	public function getResponseNegotiator() {
593
		if(!$this->responseNegotiator) {
594
			$controller = $this;
595
			$this->responseNegotiator = new PjaxResponseNegotiator(
596
				array(
597
					'CurrentForm' => function() use(&$controller) {
598
						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...
599
					},
600
					'Content' => function() use(&$controller) {
601
						return $controller->renderWith($controller->getTemplatesWithSuffix('_Content'));
602
					},
603
					'Breadcrumbs' => function() use (&$controller) {
604
						return $controller->renderWith('CMSBreadcrumbs');
605
					},
606
					'default' => function() use(&$controller) {
607
						return $controller->renderWith($controller->getViewer('show'));
608
					}
609
				),
610
				$this->getResponse()
611
			);
612
		}
613
		return $this->responseNegotiator;
614
	}
615
616
	//------------------------------------------------------------------------------------------//
617
	// Main UI components
618
619
	/**
620
	 * Returns the main menu of the CMS.  This is also used by init()
621
	 * to work out which sections the user has access to.
622
	 *
623
	 * @param Boolean
624
	 * @return SS_List
625
	 */
626
	public function MainMenu($cached = true) {
627
		if(!isset($this->_cache_MainMenu) || !$cached) {
628
			// Don't accidentally return a menu if you're not logged in - it's used to determine access.
629
			if(!Member::currentUser()) return new ArrayList();
630
631
			// Encode into DO set
632
			$menu = new ArrayList();
633
			$menuItems = CMSMenu::get_viewable_menu_items();
634
635
			// extra styling for custom menu-icons
636
			$menuIconStyling = '';
637
638
			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...
639
				foreach($menuItems as $code => $menuItem) {
640
					// alternate permission checks (in addition to LeftAndMain->canView())
641
642
					if(
643
						isset($menuItem->controller)
644
						&& $this->hasMethod('alternateMenuDisplayCheck')
645
						&& !$this->alternateMenuDisplayCheck($menuItem->controller)
646
					) {
647
						continue;
648
					}
649
650
					$linkingmode = "link";
651
652
					if($menuItem->controller && get_class($this) == $menuItem->controller) {
653
						$linkingmode = "current";
654
					} else if(strpos($this->Link(), $menuItem->url) !== false) {
655
						if($this->Link() == $menuItem->url) {
656
							$linkingmode = "current";
657
658
						// default menu is the one with a blank {@link url_segment}
659
						} else if(singleton($menuItem->controller)->stat('url_segment') == '') {
660
							if($this->Link() == $this->stat('url_base').'/') {
661
								$linkingmode = "current";
662
							}
663
664
						} else {
665
							$linkingmode = "current";
666
						}
667
					}
668
669
					// already set in CMSMenu::populate_menu(), but from a static pre-controller
670
					// context, so doesn't respect the current user locale in _t() calls - as a workaround,
671
					// we simply call LeftAndMain::menu_title_for_class() again
672
					// if we're dealing with a controller
673
					if($menuItem->controller) {
674
						$defaultTitle = LeftAndMain::menu_title_for_class($menuItem->controller);
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

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

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

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

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

Loading history...
675
						$title = _t("{$menuItem->controller}.MENUTITLE", $defaultTitle);
676
					} else {
677
						$title = $menuItem->title;
678
					}
679
680
					// Provide styling for custom $menu-icon. Done here instead of in
681
					// CMSMenu::populate_menu(), because the icon is part of
682
					// the CMS right pane for the specified class as well...
683
					if($menuItem->controller) {
684
						$menuIcon = LeftAndMain::menu_icon_for_class($menuItem->controller);
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

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

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

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

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

Loading history...
685
						if (!empty($menuIcon)) $menuIconStyling .= $menuIcon;
686
					}
687
688
					$menu->push(new ArrayData(array(
689
						"MenuItem" => $menuItem,
690
						"AttributesHTML" => $menuItem->getAttributesHTML(),
691
						"Title" => Convert::raw2xml($title),
692
						"Code" => DBField::create_field('Text', $code),
693
						"Link" => $menuItem->url,
694
						"LinkingMode" => $linkingmode
695
					)));
696
				}
697
			}
698
			if ($menuIconStyling) Requirements::customCSS($menuIconStyling);
699
700
			$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...
701
		}
702
703
		return $this->_cache_MainMenu;
704
	}
705
706
	public function Menu() {
707
		return $this->renderWith($this->getTemplatesWithSuffix('_Menu'));
708
	}
709
710
	/**
711
	 * @todo Wrap in CMSMenu instance accessor
712
	 * @return ArrayData A single menu entry (see {@link MainMenu})
713
	 */
714
	public function MenuCurrentItem() {
715
		$items = $this->MainMenu();
716
		return $items->find('LinkingMode', 'current');
717
	}
718
719
	/**
720
	 * Return a list of appropriate templates for this class, with the given suffix using
721
	 * {@link SSViewer::get_templates_by_class()}
722
	 *
723
	 * @return array
724
	 */
725
	public function getTemplatesWithSuffix($suffix) {
726
		return SSViewer::get_templates_by_class(get_class($this), $suffix, 'LeftAndMain');
727
	}
728
729
	public function Content() {
730
		return $this->renderWith($this->getTemplatesWithSuffix('_Content'));
731
	}
732
733
	public function getRecord($id) {
734
		$className = $this->stat('tree_class');
735
		if($className && $id instanceof $className) {
736
			return $id;
737
		} else if($className && $id == 'root') {
738
			return singleton($className);
739
		} else if($className && is_numeric($id)) {
740
			return DataObject::get_by_id($className, $id);
741
		} else {
742
			return false;
743
		}
744
	}
745
746
	/**
747
	 * @return ArrayList
748
	 */
749
	public function Breadcrumbs($unlinked = false) {
750
		$defaultTitle = LeftAndMain::menu_title_for_class($this->class);
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

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

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

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

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

Loading history...
751
		$title = _t("{$this->class}.MENUTITLE", $defaultTitle);
752
		$items = new ArrayList(array(
753
			new ArrayData(array(
754
				'Title' => $title,
755
				'Link' => ($unlinked) ? false : $this->Link()
756
			))
757
		));
758
		$record = $this->currentPage();
759
		if($record && $record->exists()) {
760
			if($record->hasExtension('Hierarchy')) {
761
				$ancestors = $record->getAncestors();
762
				$ancestors = new ArrayList(array_reverse($ancestors->toArray()));
763
				$ancestors->push($record);
764
				foreach($ancestors as $ancestor) {
765
					$items->push(new ArrayData(array(
766
						'Title' => ($ancestor->MenuTitle) ? $ancestor->MenuTitle : $ancestor->Title,
767
						'Link' => ($unlinked) ? false : Controller::join_links($this->Link('show'), $ancestor->ID)
768
					)));
769
				}
770
			} else {
771
				$items->push(new ArrayData(array(
772
					'Title' => ($record->MenuTitle) ? $record->MenuTitle : $record->Title,
773
					'Link' => ($unlinked) ? false : Controller::join_links($this->Link('show'), $record->ID)
774
				)));
775
			}
776
		}
777
778
		return $items;
779
	}
780
781
	/**
782
	 * @return String HTML
783
	 */
784
	public function SiteTreeAsUL() {
785
		$html = $this->getSiteTreeFor($this->stat('tree_class'));
786
		$this->extend('updateSiteTreeAsUL', $html);
787
		return $html;
788
	}
789
790
	/**
791
	 * Gets the current search filter for this request, if available
792
	 *
793
	 * @throws InvalidArgumentException
794
	 * @return LeftAndMain_SearchFilter
795
	 */
796
	protected function getSearchFilter() {
797
		// Check for given FilterClass
798
		$params = $this->getRequest()->getVar('q');
799
		if(empty($params['FilterClass'])) {
800
			return null;
801
		}
802
803
		// Validate classname
804
		$filterClass = $params['FilterClass'];
805
		$filterInfo = new ReflectionClass($filterClass);
806
		if(!$filterInfo->implementsInterface('LeftAndMain_SearchFilter')) {
807
			throw new InvalidArgumentException(sprintf('Invalid filter class passed: %s', $filterClass));
808
		}
809
810
		return Injector::inst()->createWithArgs($filterClass, array($params));
811
	}
812
813
	/**
814
	 * Get a site tree HTML listing which displays the nodes under the given criteria.
815
	 *
816
	 * @param $className The class of the root object
817
	 * @param $rootID The ID of the root object.  If this is null then a complete tree will be
818
	 *  shown
819
	 * @param $childrenMethod The method to call to get the children of the tree. For example,
820
	 *  Children, AllChildrenIncludingDeleted, or AllHistoricalChildren
821
	 * @return String Nested unordered list with links to each page
822
	 */
823
	public function getSiteTreeFor($className, $rootID = null, $childrenMethod = null, $numChildrenMethod = null,
824
			$filterFunction = null, $nodeCountThreshold = 30) {
825
826
		// Filter criteria
827
		$filter = $this->getSearchFilter();
828
829
		// Default childrenMethod and numChildrenMethod
830
		if(!$childrenMethod) $childrenMethod = ($filter && $filter->getChildrenMethod())
831
			? $filter->getChildrenMethod()
832
			: 'AllChildrenIncludingDeleted';
833
834
		if(!$numChildrenMethod) {
835
			$numChildrenMethod = 'numChildren';
836
			if($filter && $filter->getNumChildrenMethod()) {
837
				$numChildrenMethod = $filter->getNumChildrenMethod();
838
			}
839
		}
840
		if(!$filterFunction && $filter) {
841
			$filterFunction = function($node) use($filter) {
842
				return $filter->isPageIncluded($node);
843
			};
844
		}
845
846
		// Get the tree root
847
		$record = ($rootID) ? $this->getRecord($rootID) : null;
848
		$obj = $record ? $record : singleton($className);
849
850
		// Get the current page
851
		// NOTE: This *must* be fetched before markPartialTree() is called, as this
852
		// causes the Hierarchy::$marked cache to be flushed (@see CMSMain::getRecord)
853
		// which means that deleted pages stored in the marked tree would be removed
854
		$currentPage = $this->currentPage();
855
856
		// Mark the nodes of the tree to return
857
		if ($filterFunction) $obj->setMarkingFilterFunction($filterFunction);
858
859
		$obj->markPartialTree($nodeCountThreshold, $this, $childrenMethod, $numChildrenMethod);
860
861
		// Ensure current page is exposed
862
		if($currentPage) $obj->markToExpose($currentPage);
863
864
		// NOTE: SiteTree/CMSMain coupling :-(
865
		if(class_exists('SiteTree')) {
866
			SiteTree::prepopulate_permission_cache('CanEditType', $obj->markedNodeIDs(),
867
				'SiteTree::can_edit_multiple');
868
		}
869
870
		// getChildrenAsUL is a flexible and complex way of traversing the tree
871
		$controller = $this;
872
		$recordController = ($this->stat('tree_class') == 'SiteTree') ?  singleton('CMSPageEditController') : $this;
873
		$titleFn = function(&$child, $numChildrenMethod) use(&$controller, &$recordController, $filter) {
874
			$link = Controller::join_links($recordController->Link("show"), $child->ID);
875
			$node = LeftAndMain_TreeNode::create($child, $link, $controller->isCurrentPage($child), $numChildrenMethod, $filter);
876
			return $node->forTemplate();
877
		};
878
879
		// Limit the amount of nodes shown for performance reasons.
880
		// Skip the check if we're filtering the tree, since its not clear how many children will
881
		// match the filter criteria until they're queried (and matched up with previously marked nodes).
882
		$nodeThresholdLeaf = Config::inst()->get('Hierarchy', 'node_threshold_leaf');
883
		if($nodeThresholdLeaf && !$filterFunction) {
884
			$nodeCountCallback = function($parent, $numChildren) use(&$controller, $className, $nodeThresholdLeaf) {
885
				if($className == 'SiteTree' && $parent->ID && $numChildren > $nodeThresholdLeaf) {
886
					return sprintf(
887
						'<ul><li class="readonly"><span class="item">'
888
							. '%s (<a href="%s" class="cms-panel-link" data-pjax-target="Content">%s</a>)'
889
							. '</span></li></ul>',
890
						_t('LeftAndMain.TooManyPages', 'Too many pages'),
891
						Controller::join_links(
892
							$controller->LinkWithSearch($controller->Link()), '
893
							?view=list&ParentID=' . $parent->ID
894
						),
895
						_t(
896
							'LeftAndMain.ShowAsList',
897
							'show as list',
898
							'Show large amount of pages in list instead of tree view'
899
						)
900
					);
901
				}
902
			};
903
		} else {
904
			$nodeCountCallback = null;
905
		}
906
907
		// If the amount of pages exceeds the node thresholds set, use the callback
908
		$html = null;
909
		if($obj->ParentID && $nodeCountCallback) {
910
			$html = $nodeCountCallback($obj, $obj->$numChildrenMethod());
911
		}
912
913
		// Otherwise return the actual tree (which might still filter leaf thresholds on children)
914
		if(!$html) {
915
			$html = $obj->getChildrenAsUL(
916
				"",
917
				$titleFn,
918
				singleton('CMSPagesController'),
919
				true,
920
				$childrenMethod,
921
				$numChildrenMethod,
922
				$nodeCountThreshold,
923
				$nodeCountCallback
924
			);
925
		}
926
927
		// Wrap the root if needs be.
928
		if(!$rootID) {
929
			$rootLink = $this->Link('show') . '/root';
930
931
			// This lets us override the tree title with an extension
932
			if($this->hasMethod('getCMSTreeTitle') && $customTreeTitle = $this->getCMSTreeTitle()) {
933
				$treeTitle = $customTreeTitle;
934
			} elseif(class_exists('SiteConfig')) {
935
				$siteConfig = SiteConfig::current_site_config();
936
				$treeTitle =  Convert::raw2xml($siteConfig->Title);
937
			} else {
938
				$treeTitle = '...';
939
			}
940
941
			$html = "<ul><li id=\"record-0\" data-id=\"0\" class=\"Root nodelete\"><strong>$treeTitle</strong>"
942
				. $html . "</li></ul>";
943
		}
944
945
		return $html;
946
	}
947
948
	/**
949
	 * Get a subtree underneath the request param 'ID'.
950
	 * If ID = 0, then get the whole tree.
951
	 */
952
	public function getsubtree($request) {
953
		$html = $this->getSiteTreeFor(
954
			$this->stat('tree_class'),
955
			$request->getVar('ID'),
956
			null,
957
			null,
958
			null,
959
			$request->getVar('minNodeCount')
960
		);
961
962
		// Trim off the outer tag
963
		$html = preg_replace('/^[\s\t\r\n]*<ul[^>]*>/','', $html);
964
		$html = preg_replace('/<\/ul[^>]*>[\s\t\r\n]*$/','', $html);
965
966
		return $html;
967
	}
968
969
	/**
970
	 * Allows requesting a view update on specific tree nodes.
971
	 * Similar to {@link getsubtree()}, but doesn't enforce loading
972
	 * all children with the node. Useful to refresh views after
973
	 * state modifications, e.g. saving a form.
974
	 *
975
	 * @return String JSON
976
	 */
977
	public function updatetreenodes($request) {
978
		$data = array();
979
		$ids = explode(',', $request->getVar('ids'));
980
		foreach($ids as $id) {
981
			if($id === "") continue; // $id may be a blank string, which is invalid and should be skipped over
982
983
			$record = $this->getRecord($id);
984
			if(!$record) continue; // In case a page is no longer available
985
			$recordController = ($this->stat('tree_class') == 'SiteTree')
986
				?  singleton('CMSPageEditController')
987
				: $this;
988
989
			// Find the next & previous nodes, for proper positioning (Sort isn't good enough - it's not a raw offset)
990
			// TODO: These methods should really be in hierarchy - for a start it assumes Sort exists
991
			$next = $prev = null;
992
993
			$className = $this->stat('tree_class');
994
			$next = DataObject::get($className)
995
				->filter('ParentID', $record->ParentID)
996
				->filter('Sort:GreaterThan', $record->Sort)
997
				->first();
998
999
			if (!$next) {
1000
				$prev = DataObject::get($className)
1001
					->filter('ParentID', $record->ParentID)
1002
					->filter('Sort:LessThan', $record->Sort)
1003
					->reverse()
1004
					->first();
1005
			}
1006
1007
			$link = Controller::join_links($recordController->Link("show"), $record->ID);
1008
			$html = LeftAndMain_TreeNode::create($record, $link, $this->isCurrentPage($record))
1009
				->forTemplate() . '</li>';
1010
1011
			$data[$id] = array(
1012
				'html' => $html,
1013
				'ParentID' => $record->ParentID,
1014
				'NextID' => $next ? $next->ID : null,
1015
				'PrevID' => $prev ? $prev->ID : null
1016
			);
1017
		}
1018
		$this->getResponse()->addHeader('Content-Type', 'text/json');
1019
		return Convert::raw2json($data);
1020
	}
1021
1022
	/**
1023
	 * Save  handler
1024
	 */
1025
	public function save($data, $form) {
1026
		$className = $this->stat('tree_class');
1027
1028
		// Existing or new record?
1029
		$id = $data['ID'];
1030
		if(substr($id,0,3) != 'new') {
1031
			$record = DataObject::get_by_id($className, $id);
1032
			if($record && !$record->canEdit()) return Security::permissionFailure($this);
1033
			if(!$record || !$record->ID) $this->httpError(404, "Bad record ID #" . (int)$id);
1034
		} else {
1035
			if(!singleton($this->stat('tree_class'))->canCreate()) return Security::permissionFailure($this);
1036
			$record = $this->getNewItem($id, false);
1037
		}
1038
1039
		// save form data into record
1040
		$form->saveInto($record, true);
1041
		$record->write();
1042
		$this->extend('onAfterSave', $record);
1043
		$this->setCurrentPageID($record->ID);
1044
1045
		$this->getResponse()->addHeader('X-Status', rawurlencode(_t('LeftAndMain.SAVEDUP', 'Saved.')));
1046
		return $this->getResponseNegotiator()->respond($this->getRequest());
1047
	}
1048
1049
	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...
1050
		$className = $this->stat('tree_class');
1051
1052
		$id = $data['ID'];
1053
		$record = DataObject::get_by_id($className, $id);
1054
		if($record && !$record->canDelete()) return Security::permissionFailure();
1055
		if(!$record || !$record->ID) $this->httpError(404, "Bad record ID #" . (int)$id);
1056
1057
		$record->delete();
1058
1059
		$this->getResponse()->addHeader('X-Status', rawurlencode(_t('LeftAndMain.DELETED', 'Deleted.')));
1060
		return $this->getResponseNegotiator()->respond(
1061
			$this->getRequest(),
1062
			array('currentform' => array($this, 'EmptyForm'))
1063
		);
1064
	}
1065
1066
	/**
1067
	 * Update the position and parent of a tree node.
1068
	 * Only saves the node if changes were made.
1069
	 *
1070
	 * Required data:
1071
	 * - 'ID': The moved node
1072
	 * - 'ParentID': New parent relation of the moved node (0 for root)
1073
	 * - 'SiblingIDs': Array of all sibling nodes to the moved node (incl. the node itself).
1074
	 *   In case of a 'ParentID' change, relates to the new siblings under the new parent.
1075
	 *
1076
	 * @return SS_HTTPResponse JSON string with a
1077
	 */
1078
	public function savetreenode($request) {
1079
		if (!SecurityToken::inst()->checkRequest($request)) {
1080
			return $this->httpError(400);
1081
		}
1082
		if (!Permission::check('SITETREE_REORGANISE') && !Permission::check('ADMIN')) {
1083
			$this->getResponse()->setStatusCode(
1084
				403,
1085
				_t('LeftAndMain.CANT_REORGANISE',
1086
					"You do not have permission to rearange the site tree. Your change was not saved.")
1087
			);
1088
			return;
1089
		}
1090
1091
		$className = $this->stat('tree_class');
1092
		$statusUpdates = array('modified'=>array());
1093
		$id = $request->requestVar('ID');
1094
		$parentID = $request->requestVar('ParentID');
1095
1096
		if($className == 'SiteTree' && $page = DataObject::get_by_id('Page', $id)){
1097
			$root = $page->getParentType();
1098
			if(($parentID == '0' || $root == 'root') && !SiteConfig::current_site_config()->canCreateTopLevel()){
1099
				$this->getResponse()->setStatusCode(
1100
					403,
1101
					_t('LeftAndMain.CANT_REORGANISE',
1102
						"You do not have permission to alter Top level pages. Your change was not saved.")
1103
					);
1104
				return;
1105
			}
1106
		}
1107
1108
		$siblingIDs = $request->requestVar('SiblingIDs');
1109
		$statusUpdates = array('modified'=>array());
1110
		if(!is_numeric($id) || !is_numeric($parentID)) throw new InvalidArgumentException();
1111
1112
		$node = DataObject::get_by_id($className, $id);
1113
		if($node && !$node->canEdit()) return Security::permissionFailure($this);
1114
1115
		if(!$node) {
1116
			$this->getResponse()->setStatusCode(
1117
				500,
1118
				_t('LeftAndMain.PLEASESAVE',
1119
					"Please Save Page: This page could not be updated because it hasn't been saved yet."
1120
				)
1121
			);
1122
			return;
1123
		}
1124
1125
		// Update hierarchy (only if ParentID changed)
1126
		if($node->ParentID != $parentID) {
1127
			$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...
1128
			$node->write();
1129
1130
			$statusUpdates['modified'][$node->ID] = array(
1131
				'TreeTitle'=>$node->TreeTitle
1132
			);
1133
1134
			// Update all dependent pages
1135
			if(class_exists('VirtualPage')) {
1136
				$virtualPages = VirtualPage::get()->filter("CopyContentFromID", $node->ID);
1137
				foreach($virtualPages as $virtualPage) {
1138
					$statusUpdates['modified'][$virtualPage->ID] = array(
1139
						'TreeTitle' => $virtualPage->TreeTitle()
1140
					);
1141
				}
1142
			}
1143
1144
			$this->getResponse()->addHeader('X-Status',
1145
				rawurlencode(_t('LeftAndMain.REORGANISATIONSUCCESSFUL', 'Reorganised the site tree successfully.')));
1146
		}
1147
1148
		// Update sorting
1149
		if(is_array($siblingIDs)) {
1150
			$counter = 0;
1151
			foreach($siblingIDs as $id) {
1152
				if($id == $node->ID) {
1153
					$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...
1154
					$node->write();
1155
					$statusUpdates['modified'][$node->ID] = array(
1156
						'TreeTitle' => $node->TreeTitle
1157
					);
1158
				} else if(is_numeric($id)) {
1159
					// Nodes that weren't "actually moved" shouldn't be registered as
1160
					// having been edited; do a direct SQL update instead
1161
					++$counter;
1162
					DB::prepared_query(
1163
						"UPDATE \"$className\" SET \"Sort\" = ? WHERE \"ID\" = ?",
1164
						array($counter, $id)
1165
					);
1166
				}
1167
			}
1168
1169
			$this->getResponse()->addHeader('X-Status',
1170
				rawurlencode(_t('LeftAndMain.REORGANISATIONSUCCESSFUL', 'Reorganised the site tree successfully.')));
1171
		}
1172
1173
		return Convert::raw2json($statusUpdates);
1174
	}
1175
1176
	public function CanOrganiseSitetree() {
1177
		return !Permission::check('SITETREE_REORGANISE') && !Permission::check('ADMIN') ? false : true;
1178
	}
1179
1180
	/**
1181
	 * Retrieves an edit form, either for display, or to process submitted data.
1182
	 * Also used in the template rendered through {@link Right()} in the $EditForm placeholder.
1183
	 *
1184
	 * This is a "pseudo-abstract" methoed, usually connected to a {@link getEditForm()}
1185
	 * method in an entwine subclass. This method can accept a record identifier,
1186
	 * selected either in custom logic, or through {@link currentPageID()}.
1187
	 * The form usually construct itself from {@link DataObject->getCMSFields()}
1188
	 * for the specific managed subclass defined in {@link LeftAndMain::$tree_class}.
1189
	 *
1190
	 * @param HTTPRequest $request Optionally contains an identifier for the
1191
	 *  record to load into the form.
1192
	 * @return Form Should return a form regardless wether a record has been found.
1193
	 *  Form might be readonly if the current user doesn't have the permission to edit
1194
	 *  the record.
1195
	 */
1196
	/**
1197
	 * @return Form
1198
	 */
1199
	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...
1200
		return $this->getEditForm();
1201
	}
1202
1203
	/**
1204
	 * Calls {@link SiteTree->getCMSFields()}
1205
	 *
1206
	 * @param Int $id
1207
	 * @param FieldList $fields
1208
	 * @return Form
1209
	 */
1210
	public function getEditForm($id = null, $fields = null) {
1211
		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...
1212
1213
		if(is_object($id)) {
1214
			$record = $id;
1215
		} else {
1216
			$record = $this->getRecord($id);
1217
			if($record && !$record->canView()) return Security::permissionFailure($this);
1218
		}
1219
1220
		if($record) {
1221
			$fields = ($fields) ? $fields : $record->getCMSFields();
1222
			if ($fields == null) {
1223
				user_error(
1224
					"getCMSFields() returned null  - it should return a FieldList object.
1225
					Perhaps you forgot to put a return statement at the end of your method?",
1226
					E_USER_ERROR
1227
				);
1228
			}
1229
1230
			// Add hidden fields which are required for saving the record
1231
			// and loading the UI state
1232
			if(!$fields->dataFieldByName('ClassName')) {
1233
				$fields->push(new HiddenField('ClassName'));
1234
			}
1235
1236
			$tree_class = $this->stat('tree_class');
1237
			if(
1238
				$tree_class::has_extension('Hierarchy')
1239
				&& !$fields->dataFieldByName('ParentID')
1240
			) {
1241
				$fields->push(new HiddenField('ParentID'));
1242
			}
1243
1244
			// Added in-line to the form, but plucked into different view by frontend scripts.
1245
			if(in_array('CMSPreviewable', class_implements($record))) {
1246
				$navField = new LiteralField('SilverStripeNavigator', $this->getSilverStripeNavigator());
1247
				$navField->setAllowHTML(true);
1248
				$fields->push($navField);
1249
			}
1250
1251
			if($record->hasMethod('getAllCMSActions')) {
1252
				$actions = $record->getAllCMSActions();
1253
			} else {
1254
				$actions = $record->getCMSActions();
1255
				// add default actions if none are defined
1256
				if(!$actions || !$actions->Count()) {
1257
					if($record->hasMethod('canEdit') && $record->canEdit()) {
1258
						$actions->push(
1259
							FormAction::create('save',_t('CMSMain.SAVE','Save'))
1260
								->addExtraClass('ss-ui-action-constructive')->setAttribute('data-icon', 'accept')
1261
						);
1262
					}
1263
					if($record->hasMethod('canDelete') && $record->canDelete()) {
1264
						$actions->push(
1265
							FormAction::create('delete',_t('ModelAdmin.DELETE','Delete'))
1266
								->addExtraClass('ss-ui-action-destructive')
1267
						);
1268
					}
1269
				}
1270
			}
1271
1272
			// Use <button> to allow full jQuery UI styling
1273
			$actionsFlattened = $actions->dataFields();
1274
			if($actionsFlattened) foreach($actionsFlattened as $action) $action->setUseButtonTag(true);
1275
1276
			$form = CMSForm::create(
1277
				$this, "EditForm", $fields, $actions
1278
			)->setHTMLID('Form_EditForm');
1279
			$form->setResponseNegotiator($this->getResponseNegotiator());
1280
			$form->addExtraClass('cms-edit-form');
1281
			$form->loadDataFrom($record);
1282
			$form->setTemplate($this->getTemplatesWithSuffix('_EditForm'));
1283
			$form->setAttribute('data-pjax-fragment', 'CurrentForm');
1284
1285
			// Announce the capability so the frontend can decide whether to allow preview or not.
1286
			if(in_array('CMSPreviewable', class_implements($record))) {
1287
				$form->addExtraClass('cms-previewable');
1288
			}
1289
1290
			// Set this if you want to split up tabs into a separate header row
1291
			// if($form->Fields()->hasTabset()) {
0 ignored issues
show
Unused Code Comprehensibility introduced by
74% of this comment could be valid code. Did you maybe forget this after debugging?

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

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

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

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

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

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

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

Loading history...
1293
			// }
1294
1295
			// Add a default or custom validator.
1296
			// @todo Currently the default Validator.js implementation
1297
			//  adds javascript to the document body, meaning it won't
1298
			//  be included properly if the associated fields are loaded
1299
			//  through ajax. This means only serverside validation
1300
			//  will kick in for pages+validation loaded through ajax.
1301
			//  This will be solved by using less obtrusive javascript validation
1302
			//  in the future, see http://open.silverstripe.com/ticket/2915 and
1303
			//  http://open.silverstripe.com/ticket/3386
1304
			if($record->hasMethod('getCMSValidator')) {
1305
				$validator = $record->getCMSValidator();
1306
				// The clientside (mainly LeftAndMain*.js) rely on ajax responses
1307
				// which can be evaluated as javascript, hence we need
1308
				// to override any global changes to the validation handler.
1309
				if($validator != NULL){
1310
					$form->setValidator($validator);
1311
				}
1312
			} else {
1313
				$form->unsetValidator();
1314
			}
1315
1316
			if($record->hasMethod('canEdit') && !$record->canEdit()) {
1317
				$readonlyFields = $form->Fields()->makeReadonly();
1318
				$form->setFields($readonlyFields);
1319
			}
1320
		} else {
1321
			$form = $this->EmptyForm();
1322
		}
1323
1324
		return $form;
1325
	}
1326
1327
	/**
1328
	 * Returns a placeholder form, used by {@link getEditForm()} if no record is selected.
1329
	 * Our javascript logic always requires a form to be present in the CMS interface.
1330
	 *
1331
	 * @return Form
1332
	 */
1333
	public function EmptyForm() {
1334
		$form = CMSForm::create(
1335
			$this,
1336
			"EditForm",
1337
			new FieldList(
1338
				// new HeaderField(
1339
				// 	'WelcomeHeader',
1340
				// 	$this->getApplicationName()
0 ignored issues
show
Unused Code Comprehensibility introduced by
67% of this comment could be valid code. Did you maybe forget this after debugging?

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Loading history...
1348
				// 	)
1349
				// )
1350
			),
1351
			new FieldList()
1352
		)->setHTMLID('Form_EditForm');
1353
		$form->setResponseNegotiator($this->getResponseNegotiator());
1354
		$form->unsetValidator();
1355
		$form->addExtraClass('cms-edit-form');
1356
		$form->addExtraClass('root-form');
1357
		$form->setTemplate($this->getTemplatesWithSuffix('_EditForm'));
1358
		$form->setAttribute('data-pjax-fragment', 'CurrentForm');
1359
1360
		return $form;
1361
	}
1362
1363
	/**
1364
	 * Return the CMS's HTML-editor toolbar
1365
	 */
1366
	public function EditorToolbar() {
1367
		return HtmlEditorField_Toolbar::create($this, "EditorToolbar");
1368
	}
1369
1370
	/**
1371
	 * Renders a panel containing tools which apply to all displayed
1372
	 * "content" (mostly through {@link EditForm()}), for example a tree navigation or a filter panel.
1373
	 * Auto-detects applicable templates by naming convention: "<controller classname>_Tools.ss",
1374
	 * and takes the most specific template (see {@link getTemplatesWithSuffix()}).
1375
	 * To explicitly disable the panel in the subclass, simply create a more specific, empty template.
1376
	 *
1377
	 * @return String HTML
1378
	 */
1379
	public function Tools() {
1380
		$templates = $this->getTemplatesWithSuffix('_Tools');
1381
		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...
1382
			$viewer = new SSViewer($templates);
1383
			return $viewer->process($this);
1384
		} else {
1385
			return false;
1386
		}
1387
	}
1388
1389
	/**
1390
	 * Renders a panel containing tools which apply to the currently displayed edit form.
1391
	 * The main difference to {@link Tools()} is that the panel is displayed within
1392
	 * the element structure of the form panel (rendered through {@link EditForm}).
1393
	 * This means the panel will be loaded alongside new forms, and refreshed upon save,
1394
	 * which can mean a performance hit, depending on how complex your panel logic gets.
1395
	 * Any form fields contained in the returned markup will also be submitted with the main form,
1396
	 * which might be desired depending on the implementation details.
1397
	 *
1398
	 * @return String HTML
1399
	 */
1400
	public function EditFormTools() {
1401
		$templates = $this->getTemplatesWithSuffix('_EditFormTools');
1402
		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...
1403
			$viewer = new SSViewer($templates);
1404
			return $viewer->process($this);
1405
		} else {
1406
			return false;
1407
		}
1408
	}
1409
1410
	/**
1411
	 * Batch Actions Handler
1412
	 */
1413
	public function batchactions() {
1414
		return new CMSBatchActionHandler($this, 'batchactions', $this->stat('tree_class'));
1415
	}
1416
1417
	/**
1418
	 * @return Form
1419
	 */
1420
	public function BatchActionsForm() {
1421
		$actions = $this->batchactions()->batchActionList();
1422
		$actionsMap = array('-1' => _t('LeftAndMain.DropdownBatchActionsDefault', 'Choose an action...')); // Placeholder action
1423
		foreach($actions as $action) {
1424
			$actionsMap[$action->Link] = $action->Title;
1425
		}
1426
1427
		$form = new Form(
1428
			$this,
1429
			'BatchActionsForm',
1430
			new FieldList(
1431
				new HiddenField('csvIDs'),
1432
				DropdownField::create(
1433
					'Action',
1434
					false,
1435
					$actionsMap
1436
				)
1437
					->setAttribute('autocomplete', 'off')
1438
					->setAttribute('data-placeholder', _t('LeftAndMain.DropdownBatchActionsDefault', 'Choose an action...'))
1439
			),
1440
			new FieldList(
1441
				// TODO i18n
1442
				new FormAction('submit', _t('Form.SubmitBtnLabel', "Go"))
1443
			)
1444
		);
1445
		$form->addExtraClass('cms-batch-actions nostyle');
1446
		$form->unsetValidator();
1447
1448
		$this->extend('updateBatchActionsForm', $form);
1449
		return $form;
1450
	}
1451
1452
	public function printable() {
1453
		$form = $this->getEditForm($this->currentPageID());
1454
		if(!$form) return false;
1455
1456
		$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...
1457
		$form->setActions(null);
0 ignored issues
show
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...
1458
1459
		Requirements::clear();
1460
		Requirements::css(FRAMEWORK_ADMIN_DIR . '/css/LeftAndMain_printable.css');
1461
		return array(
1462
			"PrintForm" => $form
1463
		);
1464
	}
1465
1466
	/**
1467
	 * Used for preview controls, mainly links which switch between different states of the page.
1468
	 *
1469
	 * @return ArrayData
1470
	 */
1471
	public function getSilverStripeNavigator() {
1472
		$page = $this->currentPage();
1473
		if($page) {
1474
			$navigator = new SilverStripeNavigator($page);
1475
			return $navigator->renderWith($this->getTemplatesWithSuffix('_SilverStripeNavigator'));
1476
		} else {
1477
			return false;
1478
		}
1479
	}
1480
1481
	/**
1482
	 * Identifier for the currently shown record,
1483
	 * in most cases a database ID. Inspects the following
1484
	 * sources (in this order):
1485
	 * - GET/POST parameter named 'ID'
1486
	 * - URL parameter named 'ID'
1487
	 * - Session value namespaced by classname, e.g. "CMSMain.currentPage"
1488
	 *
1489
	 * @return int
1490
	 */
1491
	public function currentPageID() {
1492
		if($this->getRequest()->requestVar('ID') && is_numeric($this->getRequest()->requestVar('ID')))	{
1493
			return $this->getRequest()->requestVar('ID');
1494
		} elseif ($this->getRequest()->requestVar('CMSMainCurrentPageID') && is_numeric($this->getRequest()->requestVar('CMSMainCurrentPageID'))) {
1495
			// see GridFieldDetailForm::ItemEditForm
1496
			return $this->getRequest()->requestVar('CMSMainCurrentPageID');
1497
		} elseif (isset($this->urlParams['ID']) && is_numeric($this->urlParams['ID'])) {
1498
			return $this->urlParams['ID'];
1499
		} elseif(Session::get($this->sessionNamespace() . ".currentPage")) {
1500
			return Session::get($this->sessionNamespace() . ".currentPage");
1501
		} else {
1502
			return null;
1503
		}
1504
	}
1505
1506
	/**
1507
	 * Forces the current page to be set in session,
1508
	 * which can be retrieved later through {@link currentPageID()}.
1509
	 * Keep in mind that setting an ID through GET/POST or
1510
	 * as a URL parameter will overrule this value.
1511
	 *
1512
	 * @param int $id
1513
	 */
1514
	public function setCurrentPageID($id) {
1515
		$id = (int)$id;
1516
		Session::set($this->sessionNamespace() . ".currentPage", $id);
1517
	}
1518
1519
	/**
1520
	 * Uses {@link getRecord()} and {@link currentPageID()}
1521
	 * to get the currently selected record.
1522
	 *
1523
	 * @return DataObject
1524
	 */
1525
	public function currentPage() {
1526
		return $this->getRecord($this->currentPageID());
1527
	}
1528
1529
	/**
1530
	 * Compares a given record to the currently selected one (if any).
1531
	 * Used for marking the current tree node.
1532
	 *
1533
	 * @return boolean
1534
	 */
1535
	public function isCurrentPage(DataObject $record) {
1536
		return ($record->ID == $this->currentPageID());
1537
	}
1538
1539
	/**
1540
	 * @return String
1541
	 */
1542
	protected function sessionNamespace() {
1543
		$override = $this->stat('session_namespace');
1544
		return $override ? $override : $this->class;
1545
	}
1546
1547
	/**
1548
	 * URL to a previewable record which is shown through this controller.
1549
	 * The controller might not have any previewable content, in which case
1550
	 * this method returns FALSE.
1551
	 *
1552
	 * @return String|boolean
1553
	 */
1554
	public function LinkPreview() {
1555
		return false;
1556
	}
1557
1558
	/**
1559
	 * Return the version number of this application.
1560
	 * Uses the number in <mymodule>/silverstripe_version
1561
	 * (automatically replaced by build scripts).
1562
	 * If silverstripe_version is empty,
1563
	 * then attempts to get it from composer.lock
1564
	 *
1565
	 * @return string
1566
	 */
1567
	public function CMSVersion() {
1568
		$versions = array();
1569
		$modules = array(
1570
			'silverstripe/framework' => array(
1571
				'title' => 'Framework',
1572
				'versionFile' => FRAMEWORK_PATH . '/silverstripe_version',
1573
			)
1574
		);
1575
		if(defined('CMS_PATH')) {
1576
			$modules['silverstripe/cms'] = array(
1577
				'title' => 'CMS',
1578
				'versionFile' => CMS_PATH . '/silverstripe_version',
1579
			);
1580
		}
1581
1582
		// Tries to obtain version number from composer.lock if it exists
1583
		$composerLockPath = BASE_PATH . '/composer.lock';
1584
		if (file_exists($composerLockPath)) {
1585
			$cache = SS_Cache::factory('LeftAndMain_CMSVersion');
1586
			$cacheKey = filemtime($composerLockPath);
1587
			$versions = $cache->load($cacheKey);
1588
			if($versions) {
1589
				$versions = json_decode($versions, true);
1590
			} else {
1591
				$versions = array();
1592
			}
1593
			if(!$versions && $jsonData = file_get_contents($composerLockPath)) {
1594
				$lockData = json_decode($jsonData);
1595
				if($lockData && isset($lockData->packages)) {
1596
					foreach ($lockData->packages as $package) {
1597
						if(
1598
							array_key_exists($package->name, $modules)
1599
							&& isset($package->version)
1600
						) {
1601
							$versions[$package->name] = $package->version;
1602
						}
1603
					}
1604
					$cache->save(json_encode($versions), $cacheKey);
1605
				}
1606
			}
1607
		}
1608
1609
		// Fall back to static version file
1610
		foreach($modules as $moduleName => $moduleSpec) {
1611
			if(!isset($versions[$moduleName])) {
1612
				if($staticVersion = file_get_contents($moduleSpec['versionFile'])) {
1613
					$versions[$moduleName] = $staticVersion;
1614
				} else {
1615
					$versions[$moduleName] = _t('LeftAndMain.VersionUnknown', 'Unknown');
1616
				}
1617
			}
1618
		}
1619
1620
		$out = array();
1621
		foreach($modules as $moduleName => $moduleSpec) {
1622
			$out[] = $modules[$moduleName]['title'] . ': ' . $versions[$moduleName];
1623
		}
1624
		return implode(', ', $out);
1625
	}
1626
1627
	/**
1628
	 * @return array
1629
	 */
1630
	public function SwitchView() {
1631
		if($page = $this->currentPage()) {
1632
			$nav = SilverStripeNavigator::get_for_record($page);
1633
			return $nav['items'];
1634
		}
1635
	}
1636
1637
	/**
1638
	 * @return SiteConfig
1639
	 */
1640
	public function SiteConfig() {
1641
		return (class_exists('SiteConfig')) ? SiteConfig::current_site_config() : null;
1642
	}
1643
1644
	/**
1645
	 * The href for the anchor on the Silverstripe logo.
1646
	 * Set by calling LeftAndMain::set_application_link()
1647
	 *
1648
	 * @config
1649
	 * @var String
1650
	 */
1651
	private static $application_link = '//www.silverstripe.org/';
1652
1653
	/**
1654
	 * Sets the href for the anchor on the Silverstripe logo in the menu
1655
	 *
1656
	 * @deprecated since version 4.0
1657
	 *
1658
	 * @param String $link
1659
	 */
1660
	public static function set_application_link($link) {
1661
		Deprecation::notice('4.0', 'Use the "LeftAndMain.application_link" config setting instead');
1662
		Config::inst()->update('LeftAndMain', 'application_link', $link);
1663
	}
1664
1665
	/**
1666
	 * @return String
1667
	 */
1668
	public function ApplicationLink() {
1669
		return $this->stat('application_link');
1670
	}
1671
1672
	/**
1673
	 * The application name. Customisable by calling
1674
	 * LeftAndMain::setApplicationName() - the first parameter.
1675
	 *
1676
	 * @config
1677
	 * @var String
1678
	 */
1679
	private static $application_name = 'SilverStripe';
1680
1681
	/**
1682
	 * @param String $name
1683
	 * @deprecated since version 4.0
1684
	 */
1685
	public static function setApplicationName($name) {
1686
		Deprecation::notice('4.0', 'Use the "LeftAndMain.application_name" config setting instead');
1687
		Config::inst()->update('LeftAndMain', 'application_name', $name);
1688
	}
1689
1690
	/**
1691
	 * Get the application name.
1692
	 *
1693
	 * @return string
1694
	 */
1695
	public function getApplicationName() {
1696
		return $this->stat('application_name');
1697
	}
1698
1699
	/**
1700
	 * @return string
1701
	 */
1702
	public function Title() {
1703
		$app = $this->getApplicationName();
1704
1705
		return ($section = $this->SectionTitle()) ? sprintf('%s - %s', $app, $section) : $app;
1706
	}
1707
1708
	/**
1709
	 * Return the title of the current section. Either this is pulled from
1710
	 * the current panel's menu_title or from the first active menu
1711
	 *
1712
	 * @return string
1713
	 */
1714
	public function SectionTitle() {
1715
		$class = get_class($this);
1716
		$defaultTitle = LeftAndMain::menu_title_for_class($class);
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

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

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

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

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

Loading history...
1717
		if($title = _t("{$class}.MENUTITLE", $defaultTitle)) return $title;
1718
1719
		foreach($this->MainMenu() as $menuItem) {
1720
			if($menuItem->LinkingMode != 'link') return $menuItem->Title;
1721
		}
1722
	}
1723
1724
	/**
1725
	 * Return the base directory of the tiny_mce codebase
1726
	 */
1727
	public function MceRoot() {
1728
		return MCE_ROOT;
1729
	}
1730
1731
	/**
1732
	 * Same as {@link ViewableData->CSSClasses()}, but with a changed name
1733
	 * to avoid problems when using {@link ViewableData->customise()}
1734
	 * (which always returns "ArrayData" from the $original object).
1735
	 *
1736
	 * @return String
1737
	 */
1738
	public function BaseCSSClasses() {
1739
		return $this->CSSClasses('Controller');
1740
	}
1741
1742
	/**
1743
	 * @return String
1744
	 */
1745
	public function Locale() {
1746
		return DBField::create_field('DBLocale', i18n::get_locale());
1747
	}
1748
1749
	public function providePermissions() {
1750
		$perms = array(
1751
			"CMS_ACCESS_LeftAndMain" => array(
1752
				'name' => _t('CMSMain.ACCESSALLINTERFACES', 'Access to all CMS sections'),
1753
				'category' => _t('Permission.CMS_ACCESS_CATEGORY', 'CMS Access'),
1754
				'help' => _t('CMSMain.ACCESSALLINTERFACESHELP', 'Overrules more specific access settings.'),
1755
				'sort' => -100
1756
			)
1757
		);
1758
1759
		// Add any custom ModelAdmin subclasses. Can't put this on ModelAdmin itself
1760
		// since its marked abstract, and needs to be singleton instanciated.
1761
		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...
1762
			if($class == 'ModelAdmin') continue;
1763
			if(ClassInfo::classImplements($class, 'TestOnly')) continue;
1764
1765
			$title = _t("{$class}.MENUTITLE", LeftAndMain::menu_title_for_class($class));
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

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

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

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

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

Loading history...
1766
			$perms["CMS_ACCESS_" . $class] = array(
1767
				'name' => _t(
1768
					'CMSMain.ACCESS',
1769
					"Access to '{title}' section",
1770
					"Item in permission selection identifying the admin section. Example: Access to 'Files & Images'",
1771
					array('title' => $title)
1772
				),
1773
				'category' => _t('Permission.CMS_ACCESS_CATEGORY', 'CMS Access')
1774
			);
1775
		}
1776
1777
		return $perms;
1778
	}
1779
1780
	/**
1781
	 * Register the given javascript file as required in the CMS.
1782
	 * Filenames should be relative to the base, eg, FRAMEWORK_DIR . '/javascript/loader.js'
1783
	 *
1784
	 * @deprecated since version 4.0
1785
	 */
1786
	public static function require_javascript($file) {
1787
		Deprecation::notice('4.0', 'Use "LeftAndMain.extra_requirements_javascript" config setting instead');
1788
		Config::inst()->update('LeftAndMain', 'extra_requirements_javascript', array($file => array()));
1789
	}
1790
1791
	/**
1792
	 * Register the given stylesheet file as required.
1793
	 * @deprecated since version 4.0
1794
	 *
1795
	 * @param $file String Filenames should be relative to the base, eg, THIRDPARTY_DIR . '/tree/tree.css'
1796
	 * @param $media String Comma-separated list of media-types (e.g. "screen,projector")
1797
	 * @see http://www.w3.org/TR/REC-CSS2/media.html
1798
	 */
1799
	public static function require_css($file, $media = null) {
1800
		Deprecation::notice('4.0', 'Use "LeftAndMain.extra_requirements_css" config setting instead');
1801
		Config::inst()->update('LeftAndMain', 'extra_requirements_css', array($file => array('media' => $media)));
1802
	}
1803
1804
	/**
1805
	 * Register the given "themeable stylesheet" as required.
1806
	 * Themeable stylesheets have globally unique names, just like templates and PHP files.
1807
	 * Because of this, they can be replaced by similarly named CSS files in the theme directory.
1808
	 *
1809
	 * @deprecated since version 4.0
1810
	 *
1811
	 * @param $name String The identifier of the file.  For example, css/MyFile.css would have the identifier "MyFile"
1812
	 * @param $media String Comma-separated list of media-types (e.g. "screen,projector")
1813
	 */
1814
	public static function require_themed_css($name, $media = null) {
1815
		Deprecation::notice('4.0', 'Use "LeftAndMain.extra_requirements_themedCss" config setting instead');
1816
		Config::inst()->update('LeftAndMain', 'extra_requirements_themedCss', array($name => array('media' => $media)));
1817
	}
1818
1819
}
1820
1821
/**
1822
 * @package cms
1823
 * @subpackage core
1824
 */
1825
class LeftAndMainMarkingFilter {
1826
1827
	/**
1828
	 * @var array Request params (unsanitized)
1829
	 */
1830
	protected $params = array();
1831
1832
	/**
1833
	 * @param array $params Request params (unsanitized)
1834
	 */
1835
	public function __construct($params = null) {
1836
		$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...
1837
		$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...
1838
		$parents = array();
1839
1840
		$q = $this->getQuery($params);
1841
		$res = $q->execute();
1842
		if (!$res) return;
1843
1844
		// And keep a record of parents we don't need to get parents
1845
		// of themselves, as well as IDs to mark
1846
		foreach($res as $row) {
1847
			if ($row['ParentID']) $parents[$row['ParentID']] = true;
1848
			$this->ids[$row['ID']] = true;
1849
		}
1850
1851
		// We need to recurse up the tree,
1852
		// finding ParentIDs for each ID until we run out of parents
1853
		while (!empty($parents)) {
1854
			$parentsClause = DB::placeholders($parents);
1855
			$res = DB::prepared_query(
1856
				"SELECT \"ParentID\", \"ID\" FROM \"SiteTree\" WHERE \"ID\" in ($parentsClause)",
1857
				array_keys($parents)
1858
			);
1859
			$parents = array();
1860
1861
			foreach($res as $row) {
1862
				if ($row['ParentID']) $parents[$row['ParentID']] = true;
1863
				$this->ids[$row['ID']] = true;
1864
				$this->expanded[$row['ID']] = true;
1865
			}
1866
		}
1867
	}
1868
1869
	protected function getQuery($params) {
1870
		$where = array();
1871
1872
		if(isset($params['ID'])) unset($params['ID']);
1873
		if($treeClass = static::config()->tree_class) foreach($params as $name => $val) {
1874
			// Partial string match against a variety of fields
1875
			if(!empty($val) && singleton($treeClass)->hasDatabaseField($name)) {
1876
				$predicate = sprintf('"%s" LIKE ?', $name);
1877
				$where[$predicate] = "%$val%";
1878
			}
1879
		}
1880
1881
		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...
1882
			array("ParentID", "ID"),
1883
			'SiteTree',
1884
			$where
1885
		);
1886
	}
1887
1888
	public function mark($node) {
1889
		$id = $node->ID;
1890
		if(array_key_exists((int) $id, $this->expanded)) $node->markOpened();
1891
		return array_key_exists((int) $id, $this->ids) ? $this->ids[$id] : false;
1892
	}
1893
}
1894
1895
/**
1896
 * Allow overriding finished state for faux redirects.
1897
 *
1898
 * @package framework
1899
 * @subpackage admin
1900
 */
1901
class LeftAndMain_HTTPResponse extends SS_HTTPResponse {
1902
1903
	protected $isFinished = false;
1904
1905
	public function isFinished() {
1906
		return (parent::isFinished() || $this->isFinished);
1907
	}
1908
1909
	public function setIsFinished($bool) {
1910
		$this->isFinished = $bool;
1911
	}
1912
1913
}
1914
1915
/**
1916
 * Wrapper around objects being displayed in a tree.
1917
 * Caution: Volatile API.
1918
 *
1919
 * @todo Implement recursive tree node rendering.
1920
 *
1921
 * @package framework
1922
 * @subpackage admin
1923
 */
1924
class LeftAndMain_TreeNode extends ViewableData {
1925
1926
	/**
1927
	 * Object represented by this node
1928
	 *
1929
	 * @var Object
1930
	 */
1931
	protected $obj;
1932
1933
	/**
1934
	 * Edit link to the current record in the CMS
1935
	 *
1936
	 * @var string
1937
	 */
1938
	protected $link;
1939
1940
	/**
1941
	 * True if this is the currently selected node in the tree
1942
	 *
1943
	 * @var bool
1944
	 */
1945
	protected $isCurrent;
1946
1947
	/**
1948
	 * Name of method to count the number of children
1949
	 *
1950
	 * @var string
1951
	 */
1952
	protected $numChildrenMethod;
1953
1954
1955
	/**
1956
	 *
1957
	 * @var LeftAndMain_SearchFilter
1958
	 */
1959
	protected $filter;
1960
1961
	/**
1962
	 * @param Object $obj
1963
	 * @param string $link
1964
	 * @param bool $isCurrent
1965
	 * @param string $numChildrenMethod
1966
	 * @param LeftAndMain_SearchFilter $filter
1967
	 */
1968
	public function __construct($obj, $link = null, $isCurrent = false,
1969
		$numChildrenMethod = 'numChildren', $filter = null
1970
	) {
1971
		parent::__construct();
1972
		$this->obj = $obj;
1973
		$this->link = $link;
1974
		$this->isCurrent = $isCurrent;
1975
		$this->numChildrenMethod = $numChildrenMethod;
1976
		$this->filter = $filter;
1977
	}
1978
1979
	/**
1980
	 * Returns template, for further processing by {@link Hierarchy->getChildrenAsUL()}.
1981
	 * Does not include closing tag to allow this method to inject its own children.
1982
	 *
1983
	 * @todo Remove hardcoded assumptions around returning an <li>, by implementing recursive tree node rendering
1984
	 *
1985
	 * @return String
1986
	 */
1987
	public function forTemplate() {
1988
		$obj = $this->obj;
1989
		return "<li id=\"record-$obj->ID\" data-id=\"$obj->ID\" data-pagetype=\"$obj->ClassName\" class=\""
1990
			. $this->getClasses() . "\">" . "<ins class=\"jstree-icon\">&nbsp;</ins>"
1991
			. "<a href=\"" . $this->getLink() . "\" title=\"("
1992
			. trim(_t('LeftAndMain.PAGETYPE','Page type'), " :") // account for inconsistencies in translations
1993
			. ": " . $obj->i18n_singular_name() . ") $obj->Title\" ><ins class=\"jstree-icon\">&nbsp;</ins><span class=\"text\">" . ($obj->TreeTitle)
1994
			. "</span></a>";
1995
	}
1996
1997
	/**
1998
	 * Determine the CSS classes to apply to this node
1999
	 *
2000
	 * @return string
2001
	 */
2002
	public function getClasses() {
2003
		// Get classes from object
2004
		$classes = $this->obj->CMSTreeClasses($this->numChildrenMethod);
2005
		if($this->isCurrent) {
2006
			$classes .= ' current';
2007
		}
2008
		// Get status flag classes
2009
		$flags = $this->obj->hasMethod('getStatusFlags')
2010
			? $this->obj->getStatusFlags()
2011
			: false;
2012
		if ($flags) {
2013
			$statuses = array_keys($flags);
2014
			foreach ($statuses as $s) {
2015
				$classes .= ' status-' . $s;
2016
			}
2017
		}
2018
		// Get additional filter classes
2019
		if($this->filter && ($filterClasses = $this->filter->getPageClasses($this->obj))) {
2020
			if(is_array($filterClasses)) {
2021
				$filterClasses = implode(' ' . $filterClasses);
2022
			}
2023
			$classes .= ' ' . $filterClasses;
2024
		}
2025
		return $classes ?: '';
2026
	}
2027
2028
	public function getObj() {
2029
		return $this->obj;
2030
	}
2031
2032
	public function setObj($obj) {
2033
		$this->obj = $obj;
2034
		return $this;
2035
	}
2036
2037
	public function getLink() {
2038
		return $this->link;
2039
	}
2040
2041
	public function setLink($link) {
2042
		$this->link = $link;
2043
		return $this;
2044
	}
2045
2046
	public function getIsCurrent() {
2047
		return $this->isCurrent;
2048
	}
2049
2050
	public function setIsCurrent($bool) {
2051
		$this->isCurrent = $bool;
2052
		return $this;
2053
	}
2054
2055
}
2056
2057
/**
2058
 * Abstract interface for a class which may be used to filter the results displayed
2059
 * in a nested tree
2060
 */
2061
interface LeftAndMain_SearchFilter {
2062
2063
	/**
2064
	 * Method on {@link Hierarchy} objects which is used to traverse into children relationships.
2065
	 *
2066
	 * @return string
2067
	 */
2068
	public function getChildrenMethod();
2069
2070
	/**
2071
	 * Method on {@link Hierarchy} objects which is used find the number of children for a parent page
2072
	 *
2073
	 * @return string
2074
	 */
2075
	public function getNumChildrenMethod();
2076
2077
2078
	/**
2079
	 * Returns TRUE if the given page should be included in the tree.
2080
	 * Caution: Does NOT check view permissions on the page.
2081
	 *
2082
	 * @param DataObject $page
2083
	 * @return bool
2084
	 */
2085
	public function isPageIncluded($page);
2086
2087
	/**
2088
	 * Given a page, determine any additional CSS classes to apply to the tree node
2089
	 *
2090
	 * @param DataObject $page
2091
	 * @return array|string
2092
	 */
2093
	public function getPageClasses($page);
2094
}
2095