Completed
Push — master ( 01ba7c...6d88ca )
by Damian
21:36 queued 11:17
created

LeftAndMain::getSchemaForForm()   B

Complexity

Conditions 5
Paths 10

Size

Total Lines 34
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Importance

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

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

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

// Better
class Router
{
    private $host;

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

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

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

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
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...
292
		parent::init();
293
294
		Config::inst()->update('SSViewer', 'rewrite_hash_links', false);
295
		Config::inst()->update('ContentNegotiator', 'enabled', false);
296
297
		// set language
298
		$member = Member::currentUser();
299
		if(!empty($member->Locale)) i18n::set_locale($member->Locale);
300
		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...
301
		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...
302
303
		// can't be done in cms/_config.php as locale is not set yet
304
		CMSMenu::add_link(
305
			'Help',
306
			_t('LeftAndMain.HELP', 'Help', 'Menu title'),
307
			$this->config()->help_link,
308
			-2,
309
			array(
310
				'target' => '_blank'
311
			)
312
		);
313
314
		// Allow customisation of the access check by a extension
315
		// Also all the canView() check to execute Controller::redirect()
316
		if(!$this->canView() && !$this->getResponse()->isFinished()) {
317
			// When access /admin/, we should try a redirect to another part of the admin rather than be locked out
318
			$menu = $this->MainMenu();
319
			foreach($menu as $candidate) {
320
				if(
321
					$candidate->Link &&
322
					$candidate->Link != $this->Link()
323
					&& $candidate->MenuItem->controller
324
					&& singleton($candidate->MenuItem->controller)->canView()
325
				) {
326
					return $this->redirect($candidate->Link);
327
				}
328
			}
329
330
			if(Member::currentUser()) {
331
				Session::set("BackURL", null);
332
			}
333
334
			// if no alternate menu items have matched, return a permission error
335
			$messageSet = array(
336
				'default' => _t(
337
					'LeftAndMain.PERMDEFAULT',
338
					"You must be logged in to access the administration area; please enter your credentials below."
339
				),
340
				'alreadyLoggedIn' => _t(
341
					'LeftAndMain.PERMALREADY',
342
					"I'm sorry, but you can't access that part of the CMS.  If you want to log in as someone else, do"
343
					. " so below."
344
				),
345
				'logInAgain' => _t(
346
					'LeftAndMain.PERMAGAIN',
347
					"You have been logged out of the CMS.  If you would like to log in again, enter a username and"
348
					. " password below."
349
				),
350
			);
351
352
			return Security::permissionFailure($this, $messageSet);
353
		}
354
355
		// Don't continue if there's already been a redirection request.
356
		if($this->redirectedTo()) return;
357
358
		// Audit logging hook
359
		if(empty($_REQUEST['executeForm']) && !$this->getRequest()->isAjax()) $this->extend('accessedCMS');
360
361
		// Set the members html editor config
362
		if(Member::currentUser()) {
363
			HtmlEditorConfig::set_active_identifier(Member::currentUser()->getHtmlEditorConfigForCMS());
0 ignored issues
show
Bug introduced by
The method getHtmlEditorConfigForCMS() does not exist on DataObject. Did you maybe mean config()?

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

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

Loading history...
364
		}
365
366
		// Set default values in the config if missing.  These things can't be defined in the config
367
		// file because insufficient information exists when that is being processed
368
		$htmlEditorConfig = HtmlEditorConfig::get_active();
369
		$htmlEditorConfig->setOption('language', i18n::get_tinymce_lang());
370
		if(!$htmlEditorConfig->getOption('content_css')) {
371
			$cssFiles = array();
372
			$cssFiles[] = FRAMEWORK_ADMIN_DIR . '/css/editor.css';
373
374
			// Use theme from the site config
375
			if(class_exists('SiteConfig') && ($config = SiteConfig::current_site_config()) && $config->Theme) {
376
				$theme = $config->Theme;
377
			} elseif(Config::inst()->get('SSViewer', 'theme_enabled') && Config::inst()->get('SSViewer', 'theme')) {
378
				$theme = Config::inst()->get('SSViewer', 'theme');
379
			} else {
380
				$theme = false;
381
			}
382
383
			if($theme) $cssFiles[] = THEMES_DIR . "/{$theme}/css/editor.css";
384
			else if(project()) $cssFiles[] = project() . '/css/editor.css';
385
386
			// Remove files that don't exist
387
			foreach($cssFiles as $k => $cssFile) {
388
				if(!file_exists(BASE_PATH . '/' . $cssFile)) unset($cssFiles[$k]);
389
			}
390
391
			$htmlEditorConfig->setOption('content_css', implode(',', $cssFiles));
392
		}
393
394
		Requirements::javascript(FRAMEWORK_ADMIN_DIR . '/javascript/dist/bundle-lib.js', [
395
			'provides' => [
396
				THIRDPARTY_DIR . '/jquery/jquery.js',
397
				THIRDPARTY_DIR . '/jquery-ui/jquery-ui.js',
398
				THIRDPARTY_DIR . '/json-js/json2.js',
399
				THIRDPARTY_DIR . '/jquery-entwine/dist/jquery.entwine-dist.js',
400
				THIRDPARTY_DIR . '/jquery-cookie/jquery.cookie.js',
401
				THIRDPARTY_DIR . '/jquery-query/jquery.query.js',
402
				THIRDPARTY_DIR . '/jquery-form/jquery.form.js',
403
				THIRDPARTY_DIR . '/jquery-ondemand/jquery.ondemand.js',
404
				THIRDPARTY_DIR . '/jquery-changetracker/lib/jquery.changetracker.js',
405
				THIRDPARTY_DIR . '/jstree/jquery.jstree.js',
406
				FRAMEWORK_ADMIN_DIR . '/thirdparty/jquery-notice/jquery.notice.js',
407
				FRAMEWORK_ADMIN_DIR . '/thirdparty/jsizes/lib/jquery.sizes.js',
408
				FRAMEWORK_ADMIN_DIR . '/thirdparty/jlayout/lib/jlayout.border.js',
409
				FRAMEWORK_ADMIN_DIR . '/thirdparty/jlayout/lib/jquery.jlayout.js',
410
				FRAMEWORK_ADMIN_DIR . '/thirdparty/history-js/scripts/uncompressed/history.js',
411
				FRAMEWORK_ADMIN_DIR . '/thirdparty/history-js/scripts/uncompressed/history.adapter.jquery.js',
412
				FRAMEWORK_ADMIN_DIR . '/thirdparty/history-js/scripts/uncompressed/history.html4.js',
413
				FRAMEWORK_ADMIN_DIR . '/thirdparty/chosen/chosen/chosen.jquery.js',
414
				FRAMEWORK_ADMIN_DIR . '/thirdparty/jquery-hoverIntent/jquery.hoverIntent.js',
415
				FRAMEWORK_DIR . '/javascript/dist/TreeDropdownField.js',
416
				FRAMEWORK_DIR . '/javascript/dist/DateField.js',
417
				FRAMEWORK_DIR . '/javascript/dist/HtmlEditorField.js',
418
				FRAMEWORK_DIR . '/javascript/dist/TabSet.js',
419
				FRAMEWORK_DIR . '/javascript/dist/GridField.js',
420
				FRAMEWORK_DIR . '/javascript/dist/i18n.js',
421
				FRAMEWORK_ADMIN_DIR . '/javascript/dist/sspath.js',
422
				FRAMEWORK_ADMIN_DIR . '/javascript/dist/ssui.core.js',
423
			]
424
		]);
425
426
		Requirements::javascript(FRAMEWORK_ADMIN_DIR . '/javascript/dist/bundle-leftandmain.js', [
427
			'provides' => [
428
				FRAMEWORK_ADMIN_DIR . '/javascript/dist/LeftAndMain.Layout.js',
429
				FRAMEWORK_ADMIN_DIR . '/javascript/dist/LeftAndMain.js',
430
				FRAMEWORK_ADMIN_DIR . '/javascript/dist/LeftAndMain.ActionTabSet.js',
431
				FRAMEWORK_ADMIN_DIR . '/javascript/dist/LeftAndMain.Panel.js',
432
				FRAMEWORK_ADMIN_DIR . '/javascript/dist/LeftAndMain.Tree.js',
433
				FRAMEWORK_ADMIN_DIR . '/javascript/dist/LeftAndMain.Content.js',
434
				FRAMEWORK_ADMIN_DIR . '/javascript/dist/LeftAndMain.EditForm.js',
435
				FRAMEWORK_ADMIN_DIR . '/javascript/dist/LeftAndMain.Menu.js',
436
				FRAMEWORK_ADMIN_DIR . '/javascript/dist/LeftAndMain.Preview.js',
437
				FRAMEWORK_ADMIN_DIR . '/javascript/dist/LeftAndMain.BatchActions.js',
438
				FRAMEWORK_ADMIN_DIR . '/javascript/dist/LeftAndMain.FieldHelp.js',
439
				FRAMEWORK_ADMIN_DIR . '/javascript/dist/LeftAndMain.FieldDescriptionToggle.js',
440
				FRAMEWORK_ADMIN_DIR . '/javascript/dist/LeftAndMain.TreeDropdownField.js'
441
			]
442
		]);
443
444
		Requirements::add_i18n_javascript(FRAMEWORK_DIR . '/javascript/lang', false, true);
445
		Requirements::add_i18n_javascript(FRAMEWORK_ADMIN_DIR . '/javascript/lang', false, true);
446
447
		if ($this->config()->session_keepalive_ping) {
448
			Requirements::javascript(FRAMEWORK_ADMIN_DIR . '/javascript/dist/LeftAndMain.Ping.js');
449
		}
450
451
		if (Director::isDev()) {
452
			// TODO Confuses jQuery.ondemand through document.write()
453
			Requirements::javascript(THIRDPARTY_DIR . '/jquery-entwine/src/jquery.entwine.inspector.js');
454
			Requirements::javascript(FRAMEWORK_ADMIN_DIR . '/javascript/dist/leaktools.js');
455
		}
456
457
		Requirements::css(FRAMEWORK_ADMIN_DIR . '/thirdparty/jquery-notice/jquery.notice.css');
458
		Requirements::css(THIRDPARTY_DIR . '/jquery-ui-themes/smoothness/jquery-ui.css');
459
		Requirements::css(FRAMEWORK_ADMIN_DIR .'/thirdparty/chosen/chosen/chosen.css');
460
		Requirements::css(THIRDPARTY_DIR . '/jstree/themes/apple/style.css');
461
		Requirements::css(FRAMEWORK_DIR . '/css/TreeDropdownField.css');
462
		Requirements::css(FRAMEWORK_ADMIN_DIR . '/css/screen.css');
463
		Requirements::css(FRAMEWORK_DIR . '/css/GridField.css');
464
465
		// Browser-specific requirements
466
		$ie = isset($_SERVER['HTTP_USER_AGENT']) ? strpos($_SERVER['HTTP_USER_AGENT'], 'MSIE') : false;
467
		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...
468
			$version = substr($_SERVER['HTTP_USER_AGENT'], $ie + 5, 3);
469
470
			if($version == 7) {
471
				Requirements::css(FRAMEWORK_ADMIN_DIR . '/css/ie7.css');
472
			} else if($version == 8) {
473
				Requirements::css(FRAMEWORK_ADMIN_DIR . '/css/ie8.css');
474
			}
475
		}
476
477
		// Custom requirements
478
		$extraJs = $this->stat('extra_requirements_javascript');
479
480
		if($extraJs) {
481
			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...
482
				if(is_numeric($file)) {
483
					$file = $config;
484
				}
485
486
				Requirements::javascript($file);
487
			}
488
		}
489
490
		$extraCss = $this->stat('extra_requirements_css');
491
492
		if($extraCss) {
493
			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...
494
				if(is_numeric($file)) {
495
					$file = $config;
496
					$config = array();
497
				}
498
499
				Requirements::css($file, isset($config['media']) ? $config['media'] : null);
500
			}
501
		}
502
503
		$extraThemedCss = $this->stat('extra_requirements_themedCss');
504
505
		if($extraThemedCss) {
506
			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...
507
				if(is_numeric($file)) {
508
					$file = $config;
509
					$config = array();
510
				}
511
512
				Requirements::themedCSS($file, isset($config['media']) ? $config['media'] : null);
513
			}
514
		}
515
516
		$dummy = null;
517
		$this->extend('init', $dummy);
518
519
		// The user's theme shouldn't affect the CMS, if, for example, they have
520
		// replaced TableListField.ss or Form.ss.
521
		Config::inst()->update('SSViewer', 'theme_enabled', false);
522
523
		//set the reading mode for the admin to stage
524
		Versioned::reading_stage('Stage');
525
	}
526
527
	public function handleRequest(SS_HTTPRequest $request, DataModel $model = null) {
528
		try {
529
			$response = parent::handleRequest($request, $model);
0 ignored issues
show
Bug introduced by
It seems like $model defined by parameter $model on line 527 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...
530
		} catch(ValidationException $e) {
531
			// Nicer presentation of model-level validation errors
532
			$msgs = _t('LeftAndMain.ValidationError', 'Validation error') . ': '
533
				. $e->getMessage();
534
			$e = new SS_HTTPResponse_Exception($msgs, 403);
535
			$errorResponse = $e->getResponse();
536
			$errorResponse->addHeader('Content-Type', 'text/plain');
537
			$errorResponse->addHeader('X-Status', rawurlencode($msgs));
538
			$e->setResponse($errorResponse);
539
			throw $e;
540
		}
541
542
		$title = $this->Title();
543
		if(!$response->getHeader('X-Controller')) $response->addHeader('X-Controller', $this->class);
544
		if(!$response->getHeader('X-Title')) $response->addHeader('X-Title', urlencode($title));
545
546
		// Prevent clickjacking, see https://developer.mozilla.org/en-US/docs/HTTP/X-Frame-Options
547
		$originalResponse = $this->getResponse();
548
		$originalResponse->addHeader('X-Frame-Options', 'SAMEORIGIN');
549
		$originalResponse->addHeader('Vary', 'X-Requested-With');
550
551
		return $response;
552
	}
553
554
	/**
555
	 * Overloaded redirection logic to trigger a fake redirect on ajax requests.
556
	 * While this violates HTTP principles, its the only way to work around the
557
	 * fact that browsers handle HTTP redirects opaquely, no intervention via JS is possible.
558
	 * In isolation, that's not a problem - but combined with history.pushState()
559
	 * it means we would request the same redirection URL twice if we want to update the URL as well.
560
	 * See LeftAndMain.js for the required jQuery ajaxComplete handlers.
561
	 */
562
	public function redirect($url, $code=302) {
563
		if($this->getRequest()->isAjax()) {
564
			$response = $this->getResponse();
565
			$response->addHeader('X-ControllerURL', $url);
566
			if($this->getRequest()->getHeader('X-Pjax') && !$response->getHeader('X-Pjax')) {
567
				$response->addHeader('X-Pjax', $this->getRequest()->getHeader('X-Pjax'));
568
			}
569
			$newResponse = new LeftAndMain_HTTPResponse(
570
				$response->getBody(),
571
				$response->getStatusCode(),
572
				$response->getStatusDescription()
573
			);
574
			foreach($response->getHeaders() as $k => $v) {
575
				$newResponse->addHeader($k, $v);
576
			}
577
			$newResponse->setIsFinished(true);
578
			$this->setResponse($newResponse);
579
			return ''; // Actual response will be re-requested by client
580
		} else {
581
			parent::redirect($url, $code);
582
		}
583
	}
584
585
	public function index($request) {
586
		return $this->getResponseNegotiator()->respond($request);
587
	}
588
589
	/**
590
	 * If this is set to true, the "switchView" context in the
591
	 * template is shown, with links to the staging and publish site.
592
	 *
593
	 * @return boolean
594
	 */
595
	public function ShowSwitchView() {
596
		return false;
597
	}
598
599
600
	//------------------------------------------------------------------------------------------//
601
	// Main controllers
602
603
	/**
604
	 * You should implement a Link() function in your subclass of LeftAndMain,
605
	 * to point to the URL of that particular controller.
606
	 *
607
	 * @return string
608
	 */
609
	public function Link($action = null) {
610
		// Handle missing url_segments
611
		if($this->config()->url_segment) {
612
			$segment = $this->config()->get('url_segment', Config::FIRST_SET);
613
		} else {
614
			$segment = $this->class;
615
		};
616
617
		$link = Controller::join_links(
618
			$this->stat('url_base', true),
0 ignored issues
show
Unused Code introduced by
The call to LeftAndMain::stat() has too many arguments starting with true.

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

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

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

Loading history...
619
			$segment,
620
			'/', // trailing slash needed if $action is null!
621
			"$action"
622
		);
623
		$this->extend('updateLink', $link);
624
		return $link;
625
	}
626
627
	/**
628
	 * Returns the menu title for the given LeftAndMain subclass.
629
	 * Implemented static so that we can get this value without instantiating an object.
630
	 * Menu title is *not* internationalised.
631
	 */
632
	public static function menu_title_for_class($class) {
633
		$title = Config::inst()->get($class, 'menu_title', Config::FIRST_SET);
634
		if(!$title) $title = preg_replace('/Admin$/', '', $class);
635
		return $title;
636
	}
637
638
	/**
639
	 * Return styling for the menu icon, if a custom icon is set for this class
640
	 *
641
	 * Example: static $menu-icon = '/path/to/image/';
642
	 * @param string $class
643
	 * @return string
644
	 */
645
	public static function menu_icon_for_class($class) {
646
		$icon = Config::inst()->get($class, 'menu_icon', Config::FIRST_SET);
647
		if (!empty($icon)) {
648
			$class = strtolower(Convert::raw2htmlname(str_replace('\\', '-', $class)));
649
			return ".icon.icon-16.icon-{$class} { background-image: url('{$icon}'); } ";
650
		}
651
		return '';
652
	}
653
654
	public function show($request) {
655
		// TODO Necessary for TableListField URLs to work properly
656
		if($request->param('ID')) $this->setCurrentPageID($request->param('ID'));
657
		return $this->getResponseNegotiator()->respond($request);
658
	}
659
660
	/**
661
	 * Caution: Volatile API.
662
	 *
663
	 * @return PjaxResponseNegotiator
664
	 */
665
	public function getResponseNegotiator() {
666
		if(!$this->responseNegotiator) {
667
			$controller = $this;
668
			$this->responseNegotiator = new PjaxResponseNegotiator(
669
				array(
670
					'CurrentForm' => function() use(&$controller) {
671
						return $controller->getEditForm()->forTemplate();
672
					},
673
					'Content' => function() use(&$controller) {
674
						return $controller->renderWith($controller->getTemplatesWithSuffix('_Content'));
675
					},
676
					'Breadcrumbs' => function() use (&$controller) {
677
						return $controller->renderWith('CMSBreadcrumbs');
678
					},
679
					'default' => function() use(&$controller) {
680
						return $controller->renderWith($controller->getViewer('show'));
681
					}
682
				),
683
				$this->getResponse()
684
			);
685
		}
686
		return $this->responseNegotiator;
687
	}
688
689
	//------------------------------------------------------------------------------------------//
690
	// Main UI components
691
692
	/**
693
	 * Returns the main menu of the CMS.  This is also used by init()
694
	 * to work out which sections the user has access to.
695
	 *
696
	 * @param Boolean
697
	 * @return SS_List
698
	 */
699
	public function MainMenu($cached = true) {
700
		if(!isset($this->_cache_MainMenu) || !$cached) {
701
			// Don't accidentally return a menu if you're not logged in - it's used to determine access.
702
			if(!Member::currentUser()) return new ArrayList();
703
704
			// Encode into DO set
705
			$menu = new ArrayList();
706
			$menuItems = CMSMenu::get_viewable_menu_items();
707
708
			// extra styling for custom menu-icons
709
			$menuIconStyling = '';
710
711
			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...
712
				foreach($menuItems as $code => $menuItem) {
713
					// alternate permission checks (in addition to LeftAndMain->canView())
714
715
					if(
716
						isset($menuItem->controller)
717
						&& $this->hasMethod('alternateMenuDisplayCheck')
718
						&& !$this->alternateMenuDisplayCheck($menuItem->controller)
719
					) {
720
						continue;
721
					}
722
723
					$linkingmode = "link";
724
725
					if($menuItem->controller && get_class($this) == $menuItem->controller) {
726
						$linkingmode = "current";
727
					} else if(strpos($this->Link(), $menuItem->url) !== false) {
728
						if($this->Link() == $menuItem->url) {
729
							$linkingmode = "current";
730
731
						// default menu is the one with a blank {@link url_segment}
732
						} else if(singleton($menuItem->controller)->stat('url_segment') == '') {
733
							if($this->Link() == $this->stat('url_base').'/') {
734
								$linkingmode = "current";
735
							}
736
737
						} else {
738
							$linkingmode = "current";
739
						}
740
					}
741
742
					// already set in CMSMenu::populate_menu(), but from a static pre-controller
743
					// context, so doesn't respect the current user locale in _t() calls - as a workaround,
744
					// we simply call LeftAndMain::menu_title_for_class() again
745
					// if we're dealing with a controller
746
					if($menuItem->controller) {
747
						$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...
748
						$title = _t("{$menuItem->controller}.MENUTITLE", $defaultTitle);
749
					} else {
750
						$title = $menuItem->title;
751
					}
752
753
					// Provide styling for custom $menu-icon. Done here instead of in
754
					// CMSMenu::populate_menu(), because the icon is part of
755
					// the CMS right pane for the specified class as well...
756
					if($menuItem->controller) {
757
						$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...
758
						if (!empty($menuIcon)) $menuIconStyling .= $menuIcon;
759
					}
760
761
					$menu->push(new ArrayData(array(
762
						"MenuItem" => $menuItem,
763
						"AttributesHTML" => $menuItem->getAttributesHTML(),
764
						"Title" => Convert::raw2xml($title),
765
						"Code" => DBField::create_field('Text', $code),
766
						"Link" => $menuItem->url,
767
						"LinkingMode" => $linkingmode
768
					)));
769
				}
770
			}
771
			if ($menuIconStyling) Requirements::customCSS($menuIconStyling);
772
773
			$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...
774
		}
775
776
		return $this->_cache_MainMenu;
777
	}
778
779
	public function Menu() {
780
		return $this->renderWith($this->getTemplatesWithSuffix('_Menu'));
781
	}
782
783
	/**
784
	 * @todo Wrap in CMSMenu instance accessor
785
	 * @return ArrayData A single menu entry (see {@link MainMenu})
786
	 */
787
	public function MenuCurrentItem() {
788
		$items = $this->MainMenu();
789
		return $items->find('LinkingMode', 'current');
790
	}
791
792
	/**
793
	 * Return a list of appropriate templates for this class, with the given suffix using
794
	 * {@link SSViewer::get_templates_by_class()}
795
	 *
796
	 * @return array
797
	 */
798
	public function getTemplatesWithSuffix($suffix) {
799
		return SSViewer::get_templates_by_class(get_class($this), $suffix, 'LeftAndMain');
800
	}
801
802
	public function Content() {
803
		return $this->renderWith($this->getTemplatesWithSuffix('_Content'));
804
	}
805
806
	public function getRecord($id) {
807
		$className = $this->stat('tree_class');
808
		if($className && $id instanceof $className) {
809
			return $id;
810
		} else if($className && $id == 'root') {
811
			return singleton($className);
812
		} else if($className && is_numeric($id)) {
813
			return DataObject::get_by_id($className, $id);
814
		} else {
815
			return false;
816
		}
817
	}
818
819
	/**
820
	 * @return ArrayList
821
	 */
822
	public function Breadcrumbs($unlinked = false) {
823
		$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...
824
		$title = _t("{$this->class}.MENUTITLE", $defaultTitle);
825
		$items = new ArrayList(array(
826
			new ArrayData(array(
827
				'Title' => $title,
828
				'Link' => ($unlinked) ? false : $this->Link()
829
			))
830
		));
831
		$record = $this->currentPage();
832
		if($record && $record->exists()) {
833
			if($record->hasExtension('Hierarchy')) {
834
				$ancestors = $record->getAncestors();
835
				$ancestors = new ArrayList(array_reverse($ancestors->toArray()));
836
				$ancestors->push($record);
837
				foreach($ancestors as $ancestor) {
838
					$items->push(new ArrayData(array(
839
						'Title' => ($ancestor->MenuTitle) ? $ancestor->MenuTitle : $ancestor->Title,
840
						'Link' => ($unlinked) ? false : Controller::join_links($this->Link('show'), $ancestor->ID)
841
					)));
842
				}
843
			} else {
844
				$items->push(new ArrayData(array(
845
					'Title' => ($record->MenuTitle) ? $record->MenuTitle : $record->Title,
846
					'Link' => ($unlinked) ? false : Controller::join_links($this->Link('show'), $record->ID)
847
				)));
848
			}
849
		}
850
851
		return $items;
852
	}
853
854
	/**
855
	 * @return String HTML
856
	 */
857
	public function SiteTreeAsUL() {
858
		$html = $this->getSiteTreeFor($this->stat('tree_class'));
859
		$this->extend('updateSiteTreeAsUL', $html);
860
		return $html;
861
	}
862
863
	/**
864
	 * Gets the current search filter for this request, if available
865
	 *
866
	 * @throws InvalidArgumentException
867
	 * @return LeftAndMain_SearchFilter
868
	 */
869
	protected function getSearchFilter() {
870
		// Check for given FilterClass
871
		$params = $this->getRequest()->getVar('q');
872
		if(empty($params['FilterClass'])) {
873
			return null;
874
		}
875
876
		// Validate classname
877
		$filterClass = $params['FilterClass'];
878
		$filterInfo = new ReflectionClass($filterClass);
879
		if(!$filterInfo->implementsInterface('LeftAndMain_SearchFilter')) {
880
			throw new InvalidArgumentException(sprintf('Invalid filter class passed: %s', $filterClass));
881
		}
882
883
		return Injector::inst()->createWithArgs($filterClass, array($params));
884
	}
885
886
	/**
887
	 * Get a site tree HTML listing which displays the nodes under the given criteria.
888
	 *
889
	 * @param $className The class of the root object
890
	 * @param $rootID The ID of the root object.  If this is null then a complete tree will be
891
	 *  shown
892
	 * @param $childrenMethod The method to call to get the children of the tree. For example,
893
	 *  Children, AllChildrenIncludingDeleted, or AllHistoricalChildren
894
	 * @return String Nested unordered list with links to each page
895
	 */
896
	public function getSiteTreeFor($className, $rootID = null, $childrenMethod = null, $numChildrenMethod = null,
897
			$filterFunction = null, $nodeCountThreshold = 30) {
898
899
		// Filter criteria
900
		$filter = $this->getSearchFilter();
901
902
		// Default childrenMethod and numChildrenMethod
903
		if(!$childrenMethod) $childrenMethod = ($filter && $filter->getChildrenMethod())
904
			? $filter->getChildrenMethod()
905
			: 'AllChildrenIncludingDeleted';
906
907
		if(!$numChildrenMethod) {
908
			$numChildrenMethod = 'numChildren';
909
			if($filter && $filter->getNumChildrenMethod()) {
910
				$numChildrenMethod = $filter->getNumChildrenMethod();
911
			}
912
		}
913
		if(!$filterFunction && $filter) {
914
			$filterFunction = function($node) use($filter) {
915
				return $filter->isPageIncluded($node);
916
			};
917
		}
918
919
		// Get the tree root
920
		$record = ($rootID) ? $this->getRecord($rootID) : null;
921
		$obj = $record ? $record : singleton($className);
922
923
		// Get the current page
924
		// NOTE: This *must* be fetched before markPartialTree() is called, as this
925
		// causes the Hierarchy::$marked cache to be flushed (@see CMSMain::getRecord)
926
		// which means that deleted pages stored in the marked tree would be removed
927
		$currentPage = $this->currentPage();
928
929
		// Mark the nodes of the tree to return
930
		if ($filterFunction) $obj->setMarkingFilterFunction($filterFunction);
931
932
		$obj->markPartialTree($nodeCountThreshold, $this, $childrenMethod, $numChildrenMethod);
933
934
		// Ensure current page is exposed
935
		if($currentPage) $obj->markToExpose($currentPage);
936
937
		// NOTE: SiteTree/CMSMain coupling :-(
938
		if(class_exists('SiteTree')) {
939
			SiteTree::prepopulate_permission_cache('CanEditType', $obj->markedNodeIDs(),
940
				'SiteTree::can_edit_multiple');
941
		}
942
943
		// getChildrenAsUL is a flexible and complex way of traversing the tree
944
		$controller = $this;
945
		$recordController = ($this->stat('tree_class') == 'SiteTree') ?  singleton('CMSPageEditController') : $this;
946
		$titleFn = function(&$child, $numChildrenMethod) use(&$controller, &$recordController, $filter) {
947
			$link = Controller::join_links($recordController->Link("show"), $child->ID);
948
			$node = LeftAndMain_TreeNode::create($child, $link, $controller->isCurrentPage($child), $numChildrenMethod, $filter);
949
			return $node->forTemplate();
950
		};
951
952
		// Limit the amount of nodes shown for performance reasons.
953
		// Skip the check if we're filtering the tree, since its not clear how many children will
954
		// match the filter criteria until they're queried (and matched up with previously marked nodes).
955
		$nodeThresholdLeaf = Config::inst()->get('Hierarchy', 'node_threshold_leaf');
956
		if($nodeThresholdLeaf && !$filterFunction) {
957
			$nodeCountCallback = function($parent, $numChildren) use(&$controller, $className, $nodeThresholdLeaf) {
958
				if($className == 'SiteTree' && $parent->ID && $numChildren > $nodeThresholdLeaf) {
959
					return sprintf(
960
						'<ul><li class="readonly"><span class="item">'
961
							. '%s (<a href="%s" class="cms-panel-link" data-pjax-target="Content">%s</a>)'
962
							. '</span></li></ul>',
963
						_t('LeftAndMain.TooManyPages', 'Too many pages'),
964
						Controller::join_links(
965
							$controller->LinkWithSearch($controller->Link()), '
966
							?view=list&ParentID=' . $parent->ID
967
						),
968
						_t(
969
							'LeftAndMain.ShowAsList',
970
							'show as list',
971
							'Show large amount of pages in list instead of tree view'
972
						)
973
					);
974
				}
975
			};
976
		} else {
977
			$nodeCountCallback = null;
978
		}
979
980
		// If the amount of pages exceeds the node thresholds set, use the callback
981
		$html = null;
982
		if($obj->ParentID && $nodeCountCallback) {
983
			$html = $nodeCountCallback($obj, $obj->$numChildrenMethod());
984
		}
985
986
		// Otherwise return the actual tree (which might still filter leaf thresholds on children)
987
		if(!$html) {
988
			$html = $obj->getChildrenAsUL(
989
				"",
990
				$titleFn,
991
				singleton('CMSPagesController'),
992
				true,
993
				$childrenMethod,
994
				$numChildrenMethod,
995
				$nodeCountThreshold,
996
				$nodeCountCallback
997
			);
998
		}
999
1000
		// Wrap the root if needs be.
1001
		if(!$rootID) {
1002
			$rootLink = $this->Link('show') . '/root';
1003
1004
			// This lets us override the tree title with an extension
1005
			if($this->hasMethod('getCMSTreeTitle') && $customTreeTitle = $this->getCMSTreeTitle()) {
1006
				$treeTitle = $customTreeTitle;
1007
			} elseif(class_exists('SiteConfig')) {
1008
				$siteConfig = SiteConfig::current_site_config();
1009
				$treeTitle =  Convert::raw2xml($siteConfig->Title);
1010
			} else {
1011
				$treeTitle = '...';
1012
			}
1013
1014
			$html = "<ul><li id=\"record-0\" data-id=\"0\" class=\"Root nodelete\"><strong>$treeTitle</strong>"
1015
				. $html . "</li></ul>";
1016
		}
1017
1018
		return $html;
1019
	}
1020
1021
	/**
1022
	 * Get a subtree underneath the request param 'ID'.
1023
	 * If ID = 0, then get the whole tree.
1024
	 */
1025
	public function getsubtree($request) {
1026
		$html = $this->getSiteTreeFor(
1027
			$this->stat('tree_class'),
1028
			$request->getVar('ID'),
1029
			null,
1030
			null,
1031
			null,
1032
			$request->getVar('minNodeCount')
1033
		);
1034
1035
		// Trim off the outer tag
1036
		$html = preg_replace('/^[\s\t\r\n]*<ul[^>]*>/','', $html);
1037
		$html = preg_replace('/<\/ul[^>]*>[\s\t\r\n]*$/','', $html);
1038
1039
		return $html;
1040
	}
1041
1042
	/**
1043
	 * Allows requesting a view update on specific tree nodes.
1044
	 * Similar to {@link getsubtree()}, but doesn't enforce loading
1045
	 * all children with the node. Useful to refresh views after
1046
	 * state modifications, e.g. saving a form.
1047
	 *
1048
	 * @return String JSON
1049
	 */
1050
	public function updatetreenodes($request) {
1051
		$data = array();
1052
		$ids = explode(',', $request->getVar('ids'));
1053
		foreach($ids as $id) {
1054
			if($id === "") continue; // $id may be a blank string, which is invalid and should be skipped over
1055
1056
			$record = $this->getRecord($id);
1057
			if(!$record) continue; // In case a page is no longer available
1058
			$recordController = ($this->stat('tree_class') == 'SiteTree')
1059
				?  singleton('CMSPageEditController')
1060
				: $this;
1061
1062
			// Find the next & previous nodes, for proper positioning (Sort isn't good enough - it's not a raw offset)
1063
			// TODO: These methods should really be in hierarchy - for a start it assumes Sort exists
1064
			$next = $prev = null;
1065
1066
			$className = $this->stat('tree_class');
1067
			$next = DataObject::get($className)
1068
				->filter('ParentID', $record->ParentID)
1069
				->filter('Sort:GreaterThan', $record->Sort)
1070
				->first();
1071
1072
			if (!$next) {
1073
				$prev = DataObject::get($className)
1074
					->filter('ParentID', $record->ParentID)
1075
					->filter('Sort:LessThan', $record->Sort)
1076
					->reverse()
1077
					->first();
1078
			}
1079
1080
			$link = Controller::join_links($recordController->Link("show"), $record->ID);
1081
			$html = LeftAndMain_TreeNode::create($record, $link, $this->isCurrentPage($record))
1082
				->forTemplate() . '</li>';
1083
1084
			$data[$id] = array(
1085
				'html' => $html,
1086
				'ParentID' => $record->ParentID,
1087
				'NextID' => $next ? $next->ID : null,
1088
				'PrevID' => $prev ? $prev->ID : null
1089
			);
1090
		}
1091
		$this->getResponse()->addHeader('Content-Type', 'text/json');
1092
		return Convert::raw2json($data);
1093
	}
1094
1095
	/**
1096
	 * Save  handler
1097
	 */
1098
	public function save($data, $form) {
1099
		$request = $this->getRequest();
1100
		$className = $this->stat('tree_class');
1101
1102
		// Existing or new record?
1103
		$id = $data['ID'];
1104
		if(substr($id,0,3) != 'new') {
1105
			$record = DataObject::get_by_id($className, $id);
1106
			if($record && !$record->canEdit()) return Security::permissionFailure($this);
1107
			if(!$record || !$record->ID) $this->httpError(404, "Bad record ID #" . (int)$id);
1108
		} else {
1109
			if(!singleton($this->stat('tree_class'))->canCreate()) return Security::permissionFailure($this);
1110
			$record = $this->getNewItem($id, false);
1111
		}
1112
1113
		// save form data into record
1114
		$form->saveInto($record, true);
1115
		$record->write();
1116
		$this->extend('onAfterSave', $record);
1117
		$this->setCurrentPageID($record->ID);
1118
1119
		$this->getResponse()->addHeader('X-Status', rawurlencode(_t('LeftAndMain.SAVEDUP', 'Saved.')));
1120
1121
		if($request->getHeader('X-Formschema-Request')) {
1122
			$data = $this->getSchemaForForm($form);
1123
			$response = new SS_HTTPResponse(Convert::raw2json($data));
1124
			$response->addHeader('Content-Type', 'application/json');
1125
		} else {
1126
			$response = $this->getResponseNegotiator()->respond($request);
1127
		}
1128
1129
		return $response;
1130
	}
1131
1132
	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...
1133
		$className = $this->stat('tree_class');
1134
1135
		$id = $data['ID'];
1136
		$record = DataObject::get_by_id($className, $id);
1137
		if($record && !$record->canDelete()) return Security::permissionFailure();
1138
		if(!$record || !$record->ID) $this->httpError(404, "Bad record ID #" . (int)$id);
1139
1140
		$record->delete();
1141
1142
		$this->getResponse()->addHeader('X-Status', rawurlencode(_t('LeftAndMain.DELETED', 'Deleted.')));
1143
		return $this->getResponseNegotiator()->respond(
1144
			$this->getRequest(),
1145
			array('currentform' => array($this, 'EmptyForm'))
1146
		);
1147
	}
1148
1149
	/**
1150
	 * Update the position and parent of a tree node.
1151
	 * Only saves the node if changes were made.
1152
	 *
1153
	 * Required data:
1154
	 * - 'ID': The moved node
1155
	 * - 'ParentID': New parent relation of the moved node (0 for root)
1156
	 * - 'SiblingIDs': Array of all sibling nodes to the moved node (incl. the node itself).
1157
	 *   In case of a 'ParentID' change, relates to the new siblings under the new parent.
1158
	 *
1159
	 * @return SS_HTTPResponse JSON string with a
1160
	 */
1161
	public function savetreenode($request) {
1162
		if (!Permission::check('SITETREE_REORGANISE') && !Permission::check('ADMIN')) {
1163
			$this->getResponse()->setStatusCode(
1164
				403,
1165
				_t('LeftAndMain.CANT_REORGANISE',
1166
					"You do not have permission to rearange the site tree. Your change was not saved.")
1167
			);
1168
			return;
1169
		}
1170
1171
		$className = $this->stat('tree_class');
1172
		$statusUpdates = array('modified'=>array());
1173
		$id = $request->requestVar('ID');
1174
		$parentID = $request->requestVar('ParentID');
1175
1176
		if($className == 'SiteTree' && $page = DataObject::get_by_id('Page', $id)){
1177
			$root = $page->getParentType();
1178
			if(($parentID == '0' || $root == 'root') && !SiteConfig::current_site_config()->canCreateTopLevel()){
1179
				$this->getResponse()->setStatusCode(
1180
					403,
1181
					_t('LeftAndMain.CANT_REORGANISE',
1182
						"You do not have permission to alter Top level pages. Your change was not saved.")
1183
					);
1184
				return;
1185
			}
1186
		}
1187
1188
		$siblingIDs = $request->requestVar('SiblingIDs');
1189
		$statusUpdates = array('modified'=>array());
1190
		if(!is_numeric($id) || !is_numeric($parentID)) throw new InvalidArgumentException();
1191
1192
		$node = DataObject::get_by_id($className, $id);
1193
		if($node && !$node->canEdit()) return Security::permissionFailure($this);
1194
1195
		if(!$node) {
1196
			$this->getResponse()->setStatusCode(
1197
				500,
1198
				_t('LeftAndMain.PLEASESAVE',
1199
					"Please Save Page: This page could not be updated because it hasn't been saved yet."
1200
				)
1201
			);
1202
			return;
1203
		}
1204
1205
		// Update hierarchy (only if ParentID changed)
1206
		if($node->ParentID != $parentID) {
1207
			$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...
1208
			$node->write();
1209
1210
			$statusUpdates['modified'][$node->ID] = array(
1211
				'TreeTitle'=>$node->TreeTitle
1212
			);
1213
1214
			// Update all dependent pages
1215
			if(class_exists('VirtualPage')) {
1216
				$virtualPages = VirtualPage::get()->filter("CopyContentFromID", $node->ID);
1217
				foreach($virtualPages as $virtualPage) {
1218
					$statusUpdates['modified'][$virtualPage->ID] = array(
1219
						'TreeTitle' => $virtualPage->TreeTitle()
1220
					);
1221
				}
1222
			}
1223
1224
			$this->getResponse()->addHeader('X-Status',
1225
				rawurlencode(_t('LeftAndMain.REORGANISATIONSUCCESSFUL', 'Reorganised the site tree successfully.')));
1226
		}
1227
1228
		// Update sorting
1229
		if(is_array($siblingIDs)) {
1230
			$counter = 0;
1231
			foreach($siblingIDs as $id) {
1232
				if($id == $node->ID) {
1233
					$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...
1234
					$node->write();
1235
					$statusUpdates['modified'][$node->ID] = array(
1236
						'TreeTitle' => $node->TreeTitle
1237
					);
1238
				} else if(is_numeric($id)) {
1239
					// Nodes that weren't "actually moved" shouldn't be registered as
1240
					// having been edited; do a direct SQL update instead
1241
					++$counter;
1242
					DB::prepared_query(
1243
						"UPDATE \"$className\" SET \"Sort\" = ? WHERE \"ID\" = ?",
1244
						array($counter, $id)
1245
					);
1246
				}
1247
			}
1248
1249
			$this->getResponse()->addHeader('X-Status',
1250
				rawurlencode(_t('LeftAndMain.REORGANISATIONSUCCESSFUL', 'Reorganised the site tree successfully.')));
1251
		}
1252
1253
		return Convert::raw2json($statusUpdates);
1254
	}
1255
1256
	public function CanOrganiseSitetree() {
1257
		return !Permission::check('SITETREE_REORGANISE') && !Permission::check('ADMIN') ? false : true;
1258
	}
1259
1260
	/**
1261
	 * Retrieves an edit form, either for display, or to process submitted data.
1262
	 * Also used in the template rendered through {@link Right()} in the $EditForm placeholder.
1263
	 *
1264
	 * This is a "pseudo-abstract" methoed, usually connected to a {@link getEditForm()}
1265
	 * method in an entwine subclass. This method can accept a record identifier,
1266
	 * selected either in custom logic, or through {@link currentPageID()}.
1267
	 * The form usually construct itself from {@link DataObject->getCMSFields()}
1268
	 * for the specific managed subclass defined in {@link LeftAndMain::$tree_class}.
1269
	 *
1270
	 * @param HTTPRequest $request Optionally contains an identifier for the
1271
	 *  record to load into the form.
1272
	 * @return Form Should return a form regardless wether a record has been found.
1273
	 *  Form might be readonly if the current user doesn't have the permission to edit
1274
	 *  the record.
1275
	 */
1276
	/**
1277
	 * @return Form
1278
	 */
1279
	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...
1280
		return $this->getEditForm();
1281
	}
1282
1283
	/**
1284
	 * Calls {@link SiteTree->getCMSFields()}
1285
	 *
1286
	 * @param Int $id
1287
	 * @param FieldList $fields
1288
	 * @return Form
1289
	 */
1290
	public function getEditForm($id = null, $fields = null) {
1291
		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...
1292
1293
		if(is_object($id)) {
1294
			$record = $id;
1295
		} else {
1296
			$record = $this->getRecord($id);
1297
			if($record && !$record->canView()) return Security::permissionFailure($this);
1298
		}
1299
1300
		if($record) {
1301
			$fields = ($fields) ? $fields : $record->getCMSFields();
1302
			if ($fields == null) {
1303
				user_error(
1304
					"getCMSFields() returned null  - it should return a FieldList object.
1305
					Perhaps you forgot to put a return statement at the end of your method?",
1306
					E_USER_ERROR
1307
				);
1308
			}
1309
1310
			// Add hidden fields which are required for saving the record
1311
			// and loading the UI state
1312
			if(!$fields->dataFieldByName('ClassName')) {
1313
				$fields->push(new HiddenField('ClassName'));
1314
			}
1315
1316
			$tree_class = $this->stat('tree_class');
1317
			if(
1318
				$tree_class::has_extension('Hierarchy')
1319
				&& !$fields->dataFieldByName('ParentID')
1320
			) {
1321
				$fields->push(new HiddenField('ParentID'));
1322
			}
1323
1324
			// Added in-line to the form, but plucked into different view by frontend scripts.
1325
			if(in_array('CMSPreviewable', class_implements($record))) {
1326
				$navField = new LiteralField('SilverStripeNavigator', $this->getSilverStripeNavigator());
1327
				$navField->setAllowHTML(true);
1328
				$fields->push($navField);
1329
			}
1330
1331
			if($record->hasMethod('getAllCMSActions')) {
1332
				$actions = $record->getAllCMSActions();
1333
			} else {
1334
				$actions = $record->getCMSActions();
1335
				// add default actions if none are defined
1336
				if(!$actions || !$actions->Count()) {
1337
					if($record->hasMethod('canEdit') && $record->canEdit()) {
1338
						$actions->push(
1339
							FormAction::create('save',_t('CMSMain.SAVE','Save'))
1340
								->addExtraClass('ss-ui-action-constructive')->setAttribute('data-icon', 'accept')
1341
						);
1342
					}
1343
					if($record->hasMethod('canDelete') && $record->canDelete()) {
1344
						$actions->push(
1345
							FormAction::create('delete',_t('ModelAdmin.DELETE','Delete'))
1346
								->addExtraClass('ss-ui-action-destructive')
1347
						);
1348
					}
1349
				}
1350
			}
1351
1352
			// Use <button> to allow full jQuery UI styling
1353
			$actionsFlattened = $actions->dataFields();
1354
			if($actionsFlattened) foreach($actionsFlattened as $action) $action->setUseButtonTag(true);
1355
1356
			$negotiator = $this->getResponseNegotiator();
1357
			$form = Form::create(
1358
				$this, "EditForm", $fields, $actions
1359
			)->setHTMLID('Form_EditForm');
1360
			$form->addExtraClass('cms-edit-form');
1361
			$form->loadDataFrom($record);
1362
			$form->setTemplate($this->getTemplatesWithSuffix('_EditForm'));
1363
			$form->setAttribute('data-pjax-fragment', 'CurrentForm');
1364
			$form->setValidationResponseCallback(function() use ($negotiator, $form) {
1365
				$request = $this->getRequest();
1366
				if($request->isAjax() && $negotiator) {
1367
					$form->setupFormErrors();
1368
					$result = $form->forTemplate();
1369
1370
					return $negotiator->respond($request, array(
1371
						'CurrentForm' => function() use($result) {
1372
							return $result;
1373
						}
1374
					));
1375
				}
1376
			});
1377
1378
			// Announce the capability so the frontend can decide whether to allow preview or not.
1379
			if(in_array('CMSPreviewable', class_implements($record))) {
1380
				$form->addExtraClass('cms-previewable');
1381
			}
1382
1383
			// Set this if you want to split up tabs into a separate header row
1384
			// 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...
1385
			// 	$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...
1386
			// }
1387
1388
			// Add a default or custom validator.
1389
			// @todo Currently the default Validator.js implementation
1390
			//  adds javascript to the document body, meaning it won't
1391
			//  be included properly if the associated fields are loaded
1392
			//  through ajax. This means only serverside validation
1393
			//  will kick in for pages+validation loaded through ajax.
1394
			//  This will be solved by using less obtrusive javascript validation
1395
			//  in the future, see http://open.silverstripe.com/ticket/2915 and
1396
			//  http://open.silverstripe.com/ticket/3386
1397
			if($record->hasMethod('getCMSValidator')) {
1398
				$validator = $record->getCMSValidator();
1399
				// The clientside (mainly LeftAndMain*.js) rely on ajax responses
1400
				// which can be evaluated as javascript, hence we need
1401
				// to override any global changes to the validation handler.
1402
				if($validator != NULL){
1403
					$form->setValidator($validator);
1404
				}
1405
			} else {
1406
				$form->unsetValidator();
1407
			}
1408
1409
			if($record->hasMethod('canEdit') && !$record->canEdit()) {
1410
				$readonlyFields = $form->Fields()->makeReadonly();
1411
				$form->setFields($readonlyFields);
1412
			}
1413
		} else {
1414
			$form = $this->EmptyForm();
1415
		}
1416
1417
		return $form;
1418
	}
1419
1420
	/**
1421
	 * Returns a placeholder form, used by {@link getEditForm()} if no record is selected.
1422
	 * Our javascript logic always requires a form to be present in the CMS interface.
1423
	 *
1424
	 * @return Form
1425
	 */
1426
	public function EmptyForm() {
1427
		$form = Form::create(
1428
			$this,
1429
			"EditForm",
1430
			new FieldList(
1431
				// new HeaderField(
1432
				// 	'WelcomeHeader',
1433
				// 	$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...
1434
				// ),
1435
				// new LiteralField(
1436
				// 	'WelcomeText',
1437
				// 	sprintf('<p id="WelcomeMessage">%s %s. %s</p>',
1438
				// 		_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...
1439
				// 		$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...
1440
				// 		_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...
1441
				// 	)
1442
				// )
1443
			),
1444
			new FieldList()
1445
		)->setHTMLID('Form_EditForm');
1446
		$form->unsetValidator();
1447
		$form->addExtraClass('cms-edit-form');
1448
		$form->addExtraClass('root-form');
1449
		$form->setTemplate($this->getTemplatesWithSuffix('_EditForm'));
1450
		$form->setAttribute('data-pjax-fragment', 'CurrentForm');
1451
1452
		return $form;
1453
	}
1454
1455
	/**
1456
	 * Return the CMS's HTML-editor toolbar
1457
	 */
1458
	public function EditorToolbar() {
1459
		return HtmlEditorField_Toolbar::create($this, "EditorToolbar");
1460
	}
1461
1462
	/**
1463
	 * Renders a panel containing tools which apply to all displayed
1464
	 * "content" (mostly through {@link EditForm()}), for example a tree navigation or a filter panel.
1465
	 * Auto-detects applicable templates by naming convention: "<controller classname>_Tools.ss",
1466
	 * and takes the most specific template (see {@link getTemplatesWithSuffix()}).
1467
	 * To explicitly disable the panel in the subclass, simply create a more specific, empty template.
1468
	 *
1469
	 * @return String HTML
1470
	 */
1471
	public function Tools() {
1472
		$templates = $this->getTemplatesWithSuffix('_Tools');
1473
		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...
1474
			$viewer = new SSViewer($templates);
1475
			return $viewer->process($this);
1476
		} else {
1477
			return false;
1478
		}
1479
	}
1480
1481
	/**
1482
	 * Renders a panel containing tools which apply to the currently displayed edit form.
1483
	 * The main difference to {@link Tools()} is that the panel is displayed within
1484
	 * the element structure of the form panel (rendered through {@link EditForm}).
1485
	 * This means the panel will be loaded alongside new forms, and refreshed upon save,
1486
	 * which can mean a performance hit, depending on how complex your panel logic gets.
1487
	 * Any form fields contained in the returned markup will also be submitted with the main form,
1488
	 * which might be desired depending on the implementation details.
1489
	 *
1490
	 * @return String HTML
1491
	 */
1492
	public function EditFormTools() {
1493
		$templates = $this->getTemplatesWithSuffix('_EditFormTools');
1494
		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...
1495
			$viewer = new SSViewer($templates);
1496
			return $viewer->process($this);
1497
		} else {
1498
			return false;
1499
		}
1500
	}
1501
1502
	/**
1503
	 * Batch Actions Handler
1504
	 */
1505
	public function batchactions() {
1506
		return new CMSBatchActionHandler($this, 'batchactions', $this->stat('tree_class'));
1507
	}
1508
1509
	/**
1510
	 * @return Form
1511
	 */
1512
	public function BatchActionsForm() {
1513
		$actions = $this->batchactions()->batchActionList();
1514
		$actionsMap = array('-1' => _t('LeftAndMain.DropdownBatchActionsDefault', 'Choose an action...')); // Placeholder action
1515
		foreach($actions as $action) {
1516
			$actionsMap[$action->Link] = $action->Title;
1517
		}
1518
1519
		$form = new Form(
1520
			$this,
1521
			'BatchActionsForm',
1522
			new FieldList(
1523
				new HiddenField('csvIDs'),
1524
				DropdownField::create(
1525
					'Action',
1526
					false,
1527
					$actionsMap
1528
				)
1529
					->setAttribute('autocomplete', 'off')
1530
					->setAttribute('data-placeholder', _t('LeftAndMain.DropdownBatchActionsDefault', 'Choose an action...'))
1531
			),
1532
			new FieldList(
1533
				// TODO i18n
1534
				new FormAction('submit', _t('Form.SubmitBtnLabel', "Go"))
1535
			)
1536
		);
1537
		$form->addExtraClass('cms-batch-actions nostyle');
1538
		$form->unsetValidator();
1539
1540
		$this->extend('updateBatchActionsForm', $form);
1541
		return $form;
1542
	}
1543
1544
	public function printable() {
1545
		$form = $this->getEditForm($this->currentPageID());
1546
		if(!$form) return false;
1547
1548
		$form->transform(new PrintableTransformation());
1549
		$form->setActions(null);
1550
1551
		Requirements::clear();
1552
		Requirements::css(FRAMEWORK_ADMIN_DIR . '/css/LeftAndMain_printable.css');
1553
		return array(
1554
			"PrintForm" => $form
1555
		);
1556
	}
1557
1558
	/**
1559
	 * Used for preview controls, mainly links which switch between different states of the page.
1560
	 *
1561
	 * @return ArrayData
1562
	 */
1563
	public function getSilverStripeNavigator() {
1564
		$page = $this->currentPage();
1565
		if($page) {
1566
			$navigator = new SilverStripeNavigator($page);
1567
			return $navigator->renderWith($this->getTemplatesWithSuffix('_SilverStripeNavigator'));
1568
		} else {
1569
			return false;
1570
		}
1571
	}
1572
1573
	/**
1574
	 * Identifier for the currently shown record,
1575
	 * in most cases a database ID. Inspects the following
1576
	 * sources (in this order):
1577
	 * - GET/POST parameter named 'ID'
1578
	 * - URL parameter named 'ID'
1579
	 * - Session value namespaced by classname, e.g. "CMSMain.currentPage"
1580
	 *
1581
	 * @return int
1582
	 */
1583
	public function currentPageID() {
1584
		if($this->getRequest()->requestVar('ID') && is_numeric($this->getRequest()->requestVar('ID')))	{
1585
			return $this->getRequest()->requestVar('ID');
1586
		} elseif (isset($this->urlParams['ID']) && is_numeric($this->urlParams['ID'])) {
1587
			return $this->urlParams['ID'];
1588
		} elseif(Session::get($this->sessionNamespace() . ".currentPage")) {
1589
			return Session::get($this->sessionNamespace() . ".currentPage");
1590
		} else {
1591
			return null;
1592
		}
1593
	}
1594
1595
	/**
1596
	 * Forces the current page to be set in session,
1597
	 * which can be retrieved later through {@link currentPageID()}.
1598
	 * Keep in mind that setting an ID through GET/POST or
1599
	 * as a URL parameter will overrule this value.
1600
	 *
1601
	 * @param int $id
1602
	 */
1603
	public function setCurrentPageID($id) {
1604
		Session::set($this->sessionNamespace() . ".currentPage", $id);
1605
	}
1606
1607
	/**
1608
	 * Uses {@link getRecord()} and {@link currentPageID()}
1609
	 * to get the currently selected record.
1610
	 *
1611
	 * @return DataObject
1612
	 */
1613
	public function currentPage() {
1614
		return $this->getRecord($this->currentPageID());
1615
	}
1616
1617
	/**
1618
	 * Compares a given record to the currently selected one (if any).
1619
	 * Used for marking the current tree node.
1620
	 *
1621
	 * @return boolean
1622
	 */
1623
	public function isCurrentPage(DataObject $record) {
1624
		return ($record->ID == $this->currentPageID());
1625
	}
1626
1627
	/**
1628
	 * @return String
1629
	 */
1630
	protected function sessionNamespace() {
1631
		$override = $this->stat('session_namespace');
1632
		return $override ? $override : $this->class;
1633
	}
1634
1635
	/**
1636
	 * URL to a previewable record which is shown through this controller.
1637
	 * The controller might not have any previewable content, in which case
1638
	 * this method returns FALSE.
1639
	 *
1640
	 * @return String|boolean
1641
	 */
1642
	public function LinkPreview() {
1643
		return false;
1644
	}
1645
1646
	/**
1647
	 * Return the version number of this application.
1648
	 * Uses the number in <mymodule>/silverstripe_version
1649
	 * (automatically replaced by build scripts).
1650
	 * If silverstripe_version is empty,
1651
	 * then attempts to get it from composer.lock
1652
	 *
1653
	 * @return string
1654
	 */
1655
	public function CMSVersion() {
1656
		$versions = array();
1657
		$modules = array(
1658
			'silverstripe/framework' => array(
1659
				'title' => 'Framework',
1660
				'versionFile' => FRAMEWORK_PATH . '/silverstripe_version',
1661
			)
1662
		);
1663
		if(defined('CMS_PATH')) {
1664
			$modules['silverstripe/cms'] = array(
1665
				'title' => 'CMS',
1666
				'versionFile' => CMS_PATH . '/silverstripe_version',
1667
			);
1668
		}
1669
1670
		// Tries to obtain version number from composer.lock if it exists
1671
		$composerLockPath = BASE_PATH . '/composer.lock';
1672
		if (file_exists($composerLockPath)) {
1673
			$cache = SS_Cache::factory('LeftAndMain_CMSVersion');
1674
			$cacheKey = filemtime($composerLockPath);
1675
			$versions = $cache->load($cacheKey);
1676
			if($versions) {
1677
				$versions = json_decode($versions, true);
1678
			} else {
1679
				$versions = array();
1680
			}
1681
			if(!$versions && $jsonData = file_get_contents($composerLockPath)) {
1682
				$lockData = json_decode($jsonData);
1683
				if($lockData && isset($lockData->packages)) {
1684
					foreach ($lockData->packages as $package) {
1685
						if(
1686
							array_key_exists($package->name, $modules)
1687
							&& isset($package->version)
1688
						) {
1689
							$versions[$package->name] = $package->version;
1690
						}
1691
					}
1692
					$cache->save(json_encode($versions), $cacheKey);
1693
				}
1694
			}
1695
		}
1696
1697
		// Fall back to static version file
1698
		foreach($modules as $moduleName => $moduleSpec) {
1699
			if(!isset($versions[$moduleName])) {
1700
				if($staticVersion = file_get_contents($moduleSpec['versionFile'])) {
1701
					$versions[$moduleName] = $staticVersion;
1702
				} else {
1703
					$versions[$moduleName] = _t('LeftAndMain.VersionUnknown', 'Unknown');
1704
				}
1705
			}
1706
		}
1707
1708
		$out = array();
1709
		foreach($modules as $moduleName => $moduleSpec) {
1710
			$out[] = $modules[$moduleName]['title'] . ': ' . $versions[$moduleName];
1711
		}
1712
		return implode(', ', $out);
1713
	}
1714
1715
	/**
1716
	 * @return array
1717
	 */
1718
	public function SwitchView() {
1719
		if($page = $this->currentPage()) {
1720
			$nav = SilverStripeNavigator::get_for_record($page);
1721
			return $nav['items'];
1722
		}
1723
	}
1724
1725
	/**
1726
	 * @return SiteConfig
1727
	 */
1728
	public function SiteConfig() {
1729
		return (class_exists('SiteConfig')) ? SiteConfig::current_site_config() : null;
1730
	}
1731
1732
	/**
1733
	 * The href for the anchor on the Silverstripe logo.
1734
	 * Set by calling LeftAndMain::set_application_link()
1735
	 *
1736
	 * @config
1737
	 * @var String
1738
	 */
1739
	private static $application_link = '//www.silverstripe.org/';
1740
1741
	/**
1742
	 * Sets the href for the anchor on the Silverstripe logo in the menu
1743
	 *
1744
	 * @deprecated since version 4.0
1745
	 *
1746
	 * @param String $link
1747
	 */
1748
	public static function set_application_link($link) {
1749
		Deprecation::notice('4.0', 'Use the "LeftAndMain.application_link" config setting instead');
1750
		Config::inst()->update('LeftAndMain', 'application_link', $link);
1751
	}
1752
1753
	/**
1754
	 * @return String
1755
	 */
1756
	public function ApplicationLink() {
1757
		return $this->stat('application_link');
1758
	}
1759
1760
	/**
1761
	 * The application name. Customisable by calling
1762
	 * LeftAndMain::setApplicationName() - the first parameter.
1763
	 *
1764
	 * @config
1765
	 * @var String
1766
	 */
1767
	private static $application_name = 'SilverStripe';
1768
1769
	/**
1770
	 * @param String $name
1771
	 * @deprecated since version 4.0
1772
	 */
1773
	public static function setApplicationName($name) {
1774
		Deprecation::notice('4.0', 'Use the "LeftAndMain.application_name" config setting instead');
1775
		Config::inst()->update('LeftAndMain', 'application_name', $name);
1776
	}
1777
1778
	/**
1779
	 * Get the application name.
1780
	 *
1781
	 * @return string
1782
	 */
1783
	public function getApplicationName() {
1784
		return $this->stat('application_name');
1785
	}
1786
1787
	/**
1788
	 * @return string
1789
	 */
1790
	public function Title() {
1791
		$app = $this->getApplicationName();
1792
1793
		return ($section = $this->SectionTitle()) ? sprintf('%s - %s', $app, $section) : $app;
1794
	}
1795
1796
	/**
1797
	 * Return the title of the current section. Either this is pulled from
1798
	 * the current panel's menu_title or from the first active menu
1799
	 *
1800
	 * @return string
1801
	 */
1802
	public function SectionTitle() {
1803
		$class = get_class($this);
1804
		$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...
1805
		if($title = _t("{$class}.MENUTITLE", $defaultTitle)) return $title;
1806
1807
		foreach($this->MainMenu() as $menuItem) {
1808
			if($menuItem->LinkingMode != 'link') return $menuItem->Title;
1809
		}
1810
	}
1811
1812
	/**
1813
	 * Same as {@link ViewableData->CSSClasses()}, but with a changed name
1814
	 * to avoid problems when using {@link ViewableData->customise()}
1815
	 * (which always returns "ArrayData" from the $original object).
1816
	 *
1817
	 * @return String
1818
	 */
1819
	public function BaseCSSClasses() {
1820
		return $this->CSSClasses('Controller');
1821
	}
1822
1823
	/**
1824
	 * @return String
1825
	 */
1826
	public function Locale() {
1827
		return DBField::create_field('DBLocale', i18n::get_locale());
1828
	}
1829
1830
	public function providePermissions() {
1831
		$perms = array(
1832
			"CMS_ACCESS_LeftAndMain" => array(
1833
				'name' => _t('CMSMain.ACCESSALLINTERFACES', 'Access to all CMS sections'),
1834
				'category' => _t('Permission.CMS_ACCESS_CATEGORY', 'CMS Access'),
1835
				'help' => _t('CMSMain.ACCESSALLINTERFACESHELP', 'Overrules more specific access settings.'),
1836
				'sort' => -100
1837
			)
1838
		);
1839
1840
		// Add any custom ModelAdmin subclasses. Can't put this on ModelAdmin itself
1841
		// since its marked abstract, and needs to be singleton instanciated.
1842
		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...
1843
			if($class == 'ModelAdmin') continue;
1844
			if(ClassInfo::classImplements($class, 'TestOnly')) continue;
1845
1846
			$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...
1847
			$perms["CMS_ACCESS_" . $class] = array(
1848
				'name' => _t(
1849
					'CMSMain.ACCESS',
1850
					"Access to '{title}' section",
1851
					"Item in permission selection identifying the admin section. Example: Access to 'Files & Images'",
1852
					array('title' => $title)
1853
				),
1854
				'category' => _t('Permission.CMS_ACCESS_CATEGORY', 'CMS Access')
1855
			);
1856
		}
1857
1858
		return $perms;
1859
	}
1860
1861
	/**
1862
	 * Register the given javascript file as required in the CMS.
1863
	 * Filenames should be relative to the base, eg, FRAMEWORK_DIR . '/javascript/dist/loader.js'
1864
	 *
1865
	 * @deprecated since version 4.0
1866
	 */
1867
	public static function require_javascript($file) {
1868
		Deprecation::notice('4.0', 'Use "LeftAndMain.extra_requirements_javascript" config setting instead');
1869
		Config::inst()->update('LeftAndMain', 'extra_requirements_javascript', array($file => array()));
1870
	}
1871
1872
	/**
1873
	 * Register the given stylesheet file as required.
1874
	 * @deprecated since version 4.0
1875
	 *
1876
	 * @param $file String Filenames should be relative to the base, eg, THIRDPARTY_DIR . '/tree/tree.css'
1877
	 * @param $media String Comma-separated list of media-types (e.g. "screen,projector")
1878
	 * @see http://www.w3.org/TR/REC-CSS2/media.html
1879
	 */
1880
	public static function require_css($file, $media = null) {
1881
		Deprecation::notice('4.0', 'Use "LeftAndMain.extra_requirements_css" config setting instead');
1882
		Config::inst()->update('LeftAndMain', 'extra_requirements_css', array($file => array('media' => $media)));
1883
	}
1884
1885
	/**
1886
	 * Register the given "themeable stylesheet" as required.
1887
	 * Themeable stylesheets have globally unique names, just like templates and PHP files.
1888
	 * Because of this, they can be replaced by similarly named CSS files in the theme directory.
1889
	 *
1890
	 * @deprecated since version 4.0
1891
	 *
1892
	 * @param $name String The identifier of the file.  For example, css/MyFile.css would have the identifier "MyFile"
1893
	 * @param $media String Comma-separated list of media-types (e.g. "screen,projector")
1894
	 */
1895
	public static function require_themed_css($name, $media = null) {
1896
		Deprecation::notice('4.0', 'Use "LeftAndMain.extra_requirements_themedCss" config setting instead');
1897
		Config::inst()->update('LeftAndMain', 'extra_requirements_themedCss', array($name => array('media' => $media)));
1898
	}
1899
1900
}
1901
1902
/**
1903
 * @package cms
1904
 * @subpackage core
1905
 */
1906
class LeftAndMainMarkingFilter {
1907
1908
	/**
1909
	 * @var array Request params (unsanitized)
1910
	 */
1911
	protected $params = array();
1912
1913
	/**
1914
	 * @param array $params Request params (unsanitized)
1915
	 */
1916
	public function __construct($params = null) {
1917
		$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...
1918
		$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...
1919
		$parents = array();
1920
1921
		$q = $this->getQuery($params);
1922
		$res = $q->execute();
1923
		if (!$res) return;
1924
1925
		// And keep a record of parents we don't need to get parents
1926
		// of themselves, as well as IDs to mark
1927
		foreach($res as $row) {
1928
			if ($row['ParentID']) $parents[$row['ParentID']] = true;
1929
			$this->ids[$row['ID']] = true;
1930
		}
1931
1932
		// We need to recurse up the tree,
1933
		// finding ParentIDs for each ID until we run out of parents
1934
		while (!empty($parents)) {
1935
			$parentsClause = DB::placeholders($parents);
1936
			$res = DB::prepared_query(
1937
				"SELECT \"ParentID\", \"ID\" FROM \"SiteTree\" WHERE \"ID\" in ($parentsClause)",
1938
				array_keys($parents)
1939
			);
1940
			$parents = array();
1941
1942
			foreach($res as $row) {
1943
				if ($row['ParentID']) $parents[$row['ParentID']] = true;
1944
				$this->ids[$row['ID']] = true;
1945
				$this->expanded[$row['ID']] = true;
1946
			}
1947
		}
1948
	}
1949
1950
	protected function getQuery($params) {
1951
		$where = array();
1952
1953
		if(isset($params['ID'])) unset($params['ID']);
1954
		if($treeClass = static::config()->tree_class) foreach($params as $name => $val) {
1955
			// Partial string match against a variety of fields
1956
			if(!empty($val) && singleton($treeClass)->hasDatabaseField($name)) {
1957
				$predicate = sprintf('"%s" LIKE ?', $name);
1958
				$where[$predicate] = "%$val%";
1959
			}
1960
		}
1961
1962
		return new SQLSelect(
1963
			array("ParentID", "ID"),
1964
			'SiteTree',
1965
			$where
1966
		);
1967
	}
1968
1969
	public function mark($node) {
1970
		$id = $node->ID;
1971
		if(array_key_exists((int) $id, $this->expanded)) $node->markOpened();
1972
		return array_key_exists((int) $id, $this->ids) ? $this->ids[$id] : false;
1973
	}
1974
}
1975
1976
/**
1977
 * Allow overriding finished state for faux redirects.
1978
 *
1979
 * @package framework
1980
 * @subpackage admin
1981
 */
1982
class LeftAndMain_HTTPResponse extends SS_HTTPResponse {
1983
1984
	protected $isFinished = false;
1985
1986
	public function isFinished() {
1987
		return (parent::isFinished() || $this->isFinished);
1988
	}
1989
1990
	public function setIsFinished($bool) {
1991
		$this->isFinished = $bool;
1992
	}
1993
1994
}
1995
1996
/**
1997
 * Wrapper around objects being displayed in a tree.
1998
 * Caution: Volatile API.
1999
 *
2000
 * @todo Implement recursive tree node rendering.
2001
 *
2002
 * @package framework
2003
 * @subpackage admin
2004
 */
2005
class LeftAndMain_TreeNode extends ViewableData {
2006
2007
	/**
2008
	 * Object represented by this node
2009
	 *
2010
	 * @var Object
2011
	 */
2012
	protected $obj;
2013
2014
	/**
2015
	 * Edit link to the current record in the CMS
2016
	 *
2017
	 * @var string
2018
	 */
2019
	protected $link;
2020
2021
	/**
2022
	 * True if this is the currently selected node in the tree
2023
	 *
2024
	 * @var bool
2025
	 */
2026
	protected $isCurrent;
2027
2028
	/**
2029
	 * Name of method to count the number of children
2030
	 *
2031
	 * @var string
2032
	 */
2033
	protected $numChildrenMethod;
2034
2035
2036
	/**
2037
	 *
2038
	 * @var LeftAndMain_SearchFilter
2039
	 */
2040
	protected $filter;
2041
2042
	/**
2043
	 * @param Object $obj
2044
	 * @param string $link
2045
	 * @param bool $isCurrent
2046
	 * @param string $numChildrenMethod
2047
	 * @param LeftAndMain_SearchFilter $filter
2048
	 */
2049
	public function __construct($obj, $link = null, $isCurrent = false,
2050
		$numChildrenMethod = 'numChildren', $filter = null
2051
	) {
2052
		parent::__construct();
2053
		$this->obj = $obj;
2054
		$this->link = $link;
2055
		$this->isCurrent = $isCurrent;
2056
		$this->numChildrenMethod = $numChildrenMethod;
2057
		$this->filter = $filter;
2058
	}
2059
2060
	/**
2061
	 * Returns template, for further processing by {@link Hierarchy->getChildrenAsUL()}.
2062
	 * Does not include closing tag to allow this method to inject its own children.
2063
	 *
2064
	 * @todo Remove hardcoded assumptions around returning an <li>, by implementing recursive tree node rendering
2065
	 *
2066
	 * @return String
2067
	 */
2068
	public function forTemplate() {
2069
		$obj = $this->obj;
2070
		return "<li id=\"record-$obj->ID\" data-id=\"$obj->ID\" data-pagetype=\"$obj->ClassName\" class=\""
2071
			. $this->getClasses() . "\">" . "<ins class=\"jstree-icon\">&nbsp;</ins>"
2072
			. "<a href=\"" . $this->getLink() . "\" title=\"("
2073
			. trim(_t('LeftAndMain.PAGETYPE','Page type'), " :") // account for inconsistencies in translations
2074
			. ": " . $obj->i18n_singular_name() . ") $obj->Title\" ><ins class=\"jstree-icon\">&nbsp;</ins><span class=\"text\">" . ($obj->TreeTitle)
2075
			. "</span></a>";
2076
	}
2077
2078
	/**
2079
	 * Determine the CSS classes to apply to this node
2080
	 *
2081
	 * @return string
2082
	 */
2083
	public function getClasses() {
2084
		// Get classes from object
2085
		$classes = $this->obj->CMSTreeClasses($this->numChildrenMethod);
2086
		if($this->isCurrent) {
2087
			$classes .= ' current';
2088
		}
2089
		// Get status flag classes
2090
		$flags = $this->obj->hasMethod('getStatusFlags')
2091
			? $this->obj->getStatusFlags()
2092
			: false;
2093
		if ($flags) {
2094
			$statuses = array_keys($flags);
2095
			foreach ($statuses as $s) {
2096
				$classes .= ' status-' . $s;
2097
			}
2098
		}
2099
		// Get additional filter classes
2100
		if($this->filter && ($filterClasses = $this->filter->getPageClasses($this->obj))) {
2101
			if(is_array($filterClasses)) {
2102
				$filterClasses = implode(' ' . $filterClasses);
2103
			}
2104
			$classes .= ' ' . $filterClasses;
2105
		}
2106
		return $classes;
2107
	}
2108
2109
	public function getObj() {
2110
		return $this->obj;
2111
	}
2112
2113
	public function setObj($obj) {
2114
		$this->obj = $obj;
2115
		return $this;
2116
	}
2117
2118
	public function getLink() {
2119
		return $this->link;
2120
	}
2121
2122
	public function setLink($link) {
2123
		$this->link = $link;
2124
		return $this;
2125
	}
2126
2127
	public function getIsCurrent() {
2128
		return $this->isCurrent;
2129
	}
2130
2131
	public function setIsCurrent($bool) {
2132
		$this->isCurrent = $bool;
2133
		return $this;
2134
	}
2135
2136
}
2137
2138
/**
2139
 * Abstract interface for a class which may be used to filter the results displayed
2140
 * in a nested tree
2141
 */
2142
interface LeftAndMain_SearchFilter {
2143
2144
	/**
2145
	 * Method on {@link Hierarchy} objects which is used to traverse into children relationships.
2146
	 *
2147
	 * @return string
2148
	 */
2149
	public function getChildrenMethod();
2150
2151
	/**
2152
	 * Method on {@link Hierarchy} objects which is used find the number of children for a parent page
2153
	 *
2154
	 * @return string
2155
	 */
2156
	public function getNumChildrenMethod();
2157
2158
2159
	/**
2160
	 * Returns TRUE if the given page should be included in the tree.
2161
	 * Caution: Does NOT check view permissions on the page.
2162
	 *
2163
	 * @param DataObject $page
2164
	 * @return bool
2165
	 */
2166
	public function isPageIncluded($page);
2167
2168
	/**
2169
	 * Given a page, determine any additional CSS classes to apply to the tree node
2170
	 *
2171
	 * @param DataObject $page
2172
	 * @return array|string
2173
	 */
2174
	public function getPageClasses($page);
2175
}
2176