Completed
Push — master ( 27ce71...012526 )
by Hamish
23s
created

LeftAndMain   F

Complexity

Total Complexity 301

Size/Duplication

Total Lines 1987
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 45

Importance

Changes 1
Bugs 1 Features 0
Metric Value
c 1
b 1
f 0
dl 0
loc 1987
rs 0.5217
wmc 301
lcom 1
cbo 45

65 Methods

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

How to fix   Complexity   

Complex Class

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

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

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

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

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

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

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

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
691
			// Respect oveloading of menu_title() in subclasses
692
			return $class::menu_title(null, $localise);
693
		}
694
		if(!$class) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $class of type string|null is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

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

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

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

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