Completed
Push — master ( feca77...694826 )
by Ingo
11:47
created

LeftAndMain   F

Complexity

Total Complexity 304

Size/Duplication

Total Lines 1992
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 45

Importance

Changes 11
Bugs 1 Features 4
Metric Value
wmc 304
c 11
b 1
f 4
lcom 1
cbo 45
dl 0
loc 1992
rs 0.5217

65 Methods

Rating   Name   Duplication   Size   Complexity  
A getCombinedClientConfig() 0 17 2
A getClientConfig() 0 5 1
A getSchemaResponse() 0 10 2
B getSchemaForForm() 0 28 4
C canView() 0 26 11
C schema() 0 36 8
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
F init() 0 226 39
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 40 9
A getNewItem() 0 8 2
B delete() 0 16 5
D savetreenode() 0 94 20
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 3 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/$RecordType/$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
	 * @return SS_HTTPResponse
230
	 */
231
	public function schema($request) {
232
		$response = $this->getResponse();
233
		$formName = $request->param('FormName');
234
		$recordType = $request->param('RecordType');
235
		$itemID = $request->param('ItemID');
236
237
		if (!$formName || !$recordType) {
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
		if($itemID) {
252
			$record = $recordType::get()->byId($itemID);
253
			if(!$record) {
254
				return (new SS_HTTPResponse('Record not found', 404));
255
			}
256
			if(!$record->canView()) {
257
				return (new SS_HTTPResponse('Record not accessible', 403));
258
			}
259
			$form->loadDataFrom($record);
260
		}
261
262
		$response->addHeader('Content-Type', 'application/json');
263
		$response->setBody(Convert::raw2json($this->getSchemaForForm($form)));
264
265
		return $response;
266
	}
267
268
	/**
269
	 * Given a form, generate a response containing the requested form
270
	 * schema if X-Formschema-Request header is set.
271
	 *
272
	 * @param Form $form
273
	 * @return SS_HTTPResponse
274
	 */
275
	protected function getSchemaResponse($form) {
276
		$request = $this->getRequest();
277
		if($request->getHeader('X-Formschema-Request')) {
278
			$data = $this->getSchemaForForm($form);
279
			$response = new SS_HTTPResponse(Convert::raw2json($data));
280
			$response->addHeader('Content-Type', 'application/json');
281
			return $response;
282
		}
283
		return null;
284
	}
285
286
	/**
287
	 * Returns a representation of the provided {@link Form} as structured data,
288
	 * based on the request data.
289
	 *
290
	 * @param Form $form
291
	 * @return array
292
	 */
293
	protected function getSchemaForForm(Form $form) {
294
		$request = $this->getRequest();
295
		$return = null;
296
297
		// Valid values for the "X-Formschema-Request" header are "schema" and "state".
298
		// If either of these values are set they will be stored in the $schemaParst array
299
		// and used to construct the response body.
300
		if ($schemaHeader = $request->getHeader('X-Formschema-Request')) {
301
			$schemaParts = array_filter(explode(',', $schemaHeader), function($value) {
302
				$validHeaderValues = ['schema', 'state'];
303
				return in_array(trim($value), $validHeaderValues);
304
			});
305
		} else {
306
			$schemaParts = ['schema'];
307
		}
308
309
		$return = ['id' => $form->FormName()];
310
311
		if (in_array('schema', $schemaParts)) {
312
			$return['schema'] = $this->schema->getSchema($form);
313
		}
314
315
		if (in_array('state', $schemaParts)) {
316
			$return['state'] = $this->schema->getState($form);
317
		}
318
319
		return $return;
320
	}
321
322
	/**
323
	 * @param Member $member
324
	 * @return boolean
325
	 */
326
	public function canView($member = null) {
327
		if(!$member && $member !== FALSE) $member = Member::currentUser();
328
329
		// cms menus only for logged-in members
330
		if(!$member) return false;
331
332
		// alternative extended checks
333
		if($this->hasMethod('alternateAccessCheck')) {
334
			$alternateAllowed = $this->alternateAccessCheck();
335
			if($alternateAllowed === FALSE) return false;
336
		}
337
338
		// Check for "CMS admin" permission
339
		if(Permission::checkMember($member, "CMS_ACCESS_LeftAndMain")) return true;
340
341
		// Check for LeftAndMain sub-class permissions
342
		$codes = array();
343
		$extraCodes = $this->stat('required_permission_codes');
344
		if($extraCodes !== false) { // allow explicit FALSE to disable subclass check
345
			if($extraCodes) $codes = array_merge($codes, (array)$extraCodes);
346
			else $codes[] = "CMS_ACCESS_$this->class";
347
		}
348
		foreach($codes as $code) if(!Permission::checkMember($member, $code)) return false;
349
350
		return true;
351
	}
352
353
	/**
354
	 * @uses LeftAndMainExtension->init()
355
	 * @uses LeftAndMainExtension->accessedCMS()
356
	 * @uses CMSMenu
357
	 */
358
	public function init() {
0 ignored issues
show
Coding Style introduced by
init uses the super-global variable $_REQUEST which is generally not recommended.

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

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

// Better
class Router
{
    private $host;

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

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

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

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
359
		parent::init();
360
361
		Config::inst()->update('SSViewer', 'rewrite_hash_links', false);
362
		Config::inst()->update('ContentNegotiator', 'enabled', false);
363
364
		// set language
365
		$member = Member::currentUser();
366
		if(!empty($member->Locale)) i18n::set_locale($member->Locale);
367
		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...
368
		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...
369
370
		// can't be done in cms/_config.php as locale is not set yet
371
		CMSMenu::add_link(
372
			'Help',
373
			_t('LeftAndMain.HELP', 'Help', 'Menu title'),
374
			$this->config()->help_link,
375
			-2,
376
			array(
377
				'target' => '_blank'
378
			)
379
		);
380
381
		// Allow customisation of the access check by a extension
382
		// Also all the canView() check to execute Controller::redirect()
383
		if(!$this->canView() && !$this->getResponse()->isFinished()) {
384
			// When access /admin/, we should try a redirect to another part of the admin rather than be locked out
385
			$menu = $this->MainMenu();
386
			foreach($menu as $candidate) {
387
				if(
388
					$candidate->Link &&
389
					$candidate->Link != $this->Link()
390
					&& $candidate->MenuItem->controller
391
					&& singleton($candidate->MenuItem->controller)->canView()
392
				) {
393
					return $this->redirect($candidate->Link);
394
				}
395
			}
396
397
			if(Member::currentUser()) {
398
				Session::set("BackURL", null);
399
			}
400
401
			// if no alternate menu items have matched, return a permission error
402
			$messageSet = array(
403
				'default' => _t(
404
					'LeftAndMain.PERMDEFAULT',
405
					"You must be logged in to access the administration area; please enter your credentials below."
406
				),
407
				'alreadyLoggedIn' => _t(
408
					'LeftAndMain.PERMALREADY',
409
					"I'm sorry, but you can't access that part of the CMS.  If you want to log in as someone else, do"
410
					. " so below."
411
				),
412
				'logInAgain' => _t(
413
					'LeftAndMain.PERMAGAIN',
414
					"You have been logged out of the CMS.  If you would like to log in again, enter a username and"
415
					. " password below."
416
				),
417
			);
418
419
			return Security::permissionFailure($this, $messageSet);
420
		}
421
422
		// Don't continue if there's already been a redirection request.
423
		if($this->redirectedTo()) return;
424
425
		// Audit logging hook
426
		if(empty($_REQUEST['executeForm']) && !$this->getRequest()->isAjax()) $this->extend('accessedCMS');
427
428
		// Set the members html editor config
429
		if(Member::currentUser()) {
430
			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...
431
		}
432
433
		// Set default values in the config if missing.  These things can't be defined in the config
434
		// file because insufficient information exists when that is being processed
435
		$htmlEditorConfig = HtmlEditorConfig::get_active();
436
		$htmlEditorConfig->setOption('language', i18n::get_tinymce_lang());
437
		if(!$htmlEditorConfig->getOption('content_css')) {
438
			$cssFiles = array();
439
			$cssFiles[] = FRAMEWORK_ADMIN_DIR . '/client/dist/styles/editor.css';
440
441
			// Use theme from the site config
442
			if(class_exists('SiteConfig') && ($config = SiteConfig::current_site_config()) && $config->Theme) {
443
				$theme = $config->Theme;
444
			} elseif(Config::inst()->get('SSViewer', 'theme_enabled') && Config::inst()->get('SSViewer', 'theme')) {
445
				$theme = Config::inst()->get('SSViewer', 'theme');
446
			} else {
447
				$theme = false;
448
			}
449
450
			if($theme) $cssFiles[] = THEMES_DIR . "/{$theme}/css/editor.css";
451
			else if(project()) $cssFiles[] = project() . '/css/editor.css';
452
453
			// Remove files that don't exist
454
			foreach($cssFiles as $k => $cssFile) {
455
				if(!file_exists(BASE_PATH . '/' . $cssFile)) unset($cssFiles[$k]);
456
			}
457
458
			$htmlEditorConfig->setOption('content_css', implode(',', $cssFiles));
459
		}
460
461
		Requirements::customScript("
462
			window.ss = window.ss || {};
463
			window.ss.config = " . $this->getCombinedClientConfig() . ";
464
		");
465
466
		Requirements::javascript(FRAMEWORK_ADMIN_DIR . '/client/dist/js/bundle-lib.js', [
467
			'provides' => [
468
				THIRDPARTY_DIR . '/jquery/jquery.js',
469
				THIRDPARTY_DIR . '/jquery-ui/jquery-ui.js',
470
				THIRDPARTY_DIR . '/jquery-entwine/dist/jquery.entwine-dist.js',
471
				THIRDPARTY_DIR . '/jquery-cookie/jquery.cookie.js',
472
				THIRDPARTY_DIR . '/jquery-query/jquery.query.js',
473
				THIRDPARTY_DIR . '/jquery-form/jquery.form.js',
474
				THIRDPARTY_DIR . '/jquery-ondemand/jquery.ondemand.js',
475
				THIRDPARTY_DIR . '/jquery-changetracker/lib/jquery.changetracker.js',
476
				THIRDPARTY_DIR . '/jstree/jquery.jstree.js',
477
				FRAMEWORK_ADMIN_DIR . '/thirdparty/jquery-notice/jquery.notice.js',
478
				FRAMEWORK_ADMIN_DIR . '/thirdparty/jsizes/lib/jquery.sizes.js',
479
				FRAMEWORK_ADMIN_DIR . '/thirdparty/jlayout/lib/jlayout.border.js',
480
				FRAMEWORK_ADMIN_DIR . '/thirdparty/jlayout/lib/jquery.jlayout.js',
481
				FRAMEWORK_ADMIN_DIR . '/thirdparty/chosen/chosen/chosen.jquery.js',
482
				FRAMEWORK_ADMIN_DIR . '/thirdparty/jquery-hoverIntent/jquery.hoverIntent.js',
483
				FRAMEWORK_DIR . '/client/dist/js/TreeDropdownField.js',
484
				FRAMEWORK_DIR . '/client/dist/js/DateField.js',
485
				FRAMEWORK_DIR . '/client/dist/js/HtmlEditorField.js',
486
				FRAMEWORK_DIR . '/client/dist/js/TabSet.js',
487
				FRAMEWORK_DIR . '/client/dist/js/GridField.js',
488
				FRAMEWORK_DIR . '/client/dist/js/i18n.js',
489
				FRAMEWORK_ADMIN_DIR . '/client/dist/js/sspath.js',
490
				FRAMEWORK_ADMIN_DIR . '/client/dist/js/ssui.core.js'
491
			]
492
		]);
493
494
		Requirements::javascript(FRAMEWORK_ADMIN_DIR . '/client/dist/js/bundle-legacy.js', [
495
			'provides' => [
496
				FRAMEWORK_ADMIN_DIR . '/client/dist/js/LeftAndMain.Layout.js',
497
				FRAMEWORK_ADMIN_DIR . '/client/dist/js/LeftAndMain.js',
498
				FRAMEWORK_ADMIN_DIR . '/client/dist/js/LeftAndMain.ActionTabSet.js',
499
				FRAMEWORK_ADMIN_DIR . '/client/dist/js/LeftAndMain.Panel.js',
500
				FRAMEWORK_ADMIN_DIR . '/client/dist/js/LeftAndMain.Tree.js',
501
				FRAMEWORK_ADMIN_DIR . '/client/dist/js/LeftAndMain.Content.js',
502
				FRAMEWORK_ADMIN_DIR . '/client/dist/js/LeftAndMain.EditForm.js',
503
				FRAMEWORK_ADMIN_DIR . '/client/dist/js/LeftAndMain.Menu.js',
504
				FRAMEWORK_ADMIN_DIR . '/client/dist/js/LeftAndMain.Preview.js',
505
				FRAMEWORK_ADMIN_DIR . '/client/dist/js/LeftAndMain.BatchActions.js',
506
				FRAMEWORK_ADMIN_DIR . '/client/dist/js/LeftAndMain.FieldHelp.js',
507
				FRAMEWORK_ADMIN_DIR . '/client/dist/js/LeftAndMain.FieldDescriptionToggle.js',
508
				FRAMEWORK_ADMIN_DIR . '/client/dist/js/LeftAndMain.TreeDropdownField.js',
509
				FRAMEWORK_ADMIN_DIR . '/client/dist/js/AddToCampaignForm.js'
510
			]
511
		]);
512
513
		Requirements::add_i18n_javascript(FRAMEWORK_DIR . '/javascript/lang', false, true);
514
		Requirements::add_i18n_javascript(FRAMEWORK_ADMIN_DIR . '/client/lang', false, true);
515
516
		if ($this->config()->session_keepalive_ping) {
517
			Requirements::javascript(FRAMEWORK_ADMIN_DIR . '/client/dist/js/LeftAndMain.Ping.js');
518
		}
519
520
		if (Director::isDev()) {
521
			// TODO Confuses jQuery.ondemand through document.write()
522
			Requirements::javascript(THIRDPARTY_DIR . '/jquery-entwine/src/jquery.entwine.inspector.js');
523
			Requirements::javascript(FRAMEWORK_ADMIN_DIR . '/client/dist/js/leaktools.js');
524
		}
525
526
		Requirements::javascript(FRAMEWORK_ADMIN_DIR . '/client/dist/js/bundle-framework.js');
527
528
		Requirements::css(FRAMEWORK_ADMIN_DIR . '/thirdparty/jquery-notice/jquery.notice.css');
529
		Requirements::css(THIRDPARTY_DIR . '/jquery-ui-themes/smoothness/jquery-ui.css');
530
		Requirements::css(THIRDPARTY_DIR . '/jstree/themes/apple/style.css');
531
		Requirements::css(FRAMEWORK_DIR . '/client/dist/styles/TreeDropdownField.css');
532
		Requirements::css(FRAMEWORK_ADMIN_DIR . '/client/dist/styles/bundle.css');
533
		Requirements::css(FRAMEWORK_DIR . '/client/dist/styles/GridField.css');
534
535
		// Custom requirements
536
		$extraJs = $this->stat('extra_requirements_javascript');
537
538
		if($extraJs) {
539
			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...
540
				if(is_numeric($file)) {
541
					$file = $config;
542
				}
543
544
				Requirements::javascript($file);
545
			}
546
		}
547
548
		$extraCss = $this->stat('extra_requirements_css');
549
550
		if($extraCss) {
551
			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...
552
				if(is_numeric($file)) {
553
					$file = $config;
554
					$config = array();
555
				}
556
557
				Requirements::css($file, isset($config['media']) ? $config['media'] : null);
558
			}
559
		}
560
561
		$extraThemedCss = $this->stat('extra_requirements_themedCss');
562
563
		if($extraThemedCss) {
564
			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...
565
				if(is_numeric($file)) {
566
					$file = $config;
567
					$config = array();
568
				}
569
570
				Requirements::themedCSS($file, isset($config['media']) ? $config['media'] : null);
571
			}
572
		}
573
574
		$dummy = null;
575
		$this->extend('init', $dummy);
576
577
		// The user's theme shouldn't affect the CMS, if, for example, they have
578
		// replaced TableListField.ss or Form.ss.
579
		Config::inst()->update('SSViewer', 'theme_enabled', false);
580
581
		//set the reading mode for the admin to stage
582
		Versioned::set_stage(Versioned::DRAFT);
583
	}
584
585
	public function handleRequest(SS_HTTPRequest $request, DataModel $model = null) {
586
		try {
587
			$response = parent::handleRequest($request, $model);
0 ignored issues
show
Bug introduced by
It seems like $model defined by parameter $model on line 585 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...
588
		} catch(ValidationException $e) {
589
			// Nicer presentation of model-level validation errors
590
			$msgs = _t('LeftAndMain.ValidationError', 'Validation error') . ': '
591
				. $e->getMessage();
592
			$e = new SS_HTTPResponse_Exception($msgs, 403);
593
			$errorResponse = $e->getResponse();
594
			$errorResponse->addHeader('Content-Type', 'text/plain');
595
			$errorResponse->addHeader('X-Status', rawurlencode($msgs));
596
			$e->setResponse($errorResponse);
597
			throw $e;
598
		}
599
600
		$title = $this->Title();
601
		if(!$response->getHeader('X-Controller')) $response->addHeader('X-Controller', $this->class);
602
		if(!$response->getHeader('X-Title')) $response->addHeader('X-Title', urlencode($title));
603
604
		// Prevent clickjacking, see https://developer.mozilla.org/en-US/docs/HTTP/X-Frame-Options
605
		$originalResponse = $this->getResponse();
606
		$originalResponse->addHeader('X-Frame-Options', 'SAMEORIGIN');
607
		$originalResponse->addHeader('Vary', 'X-Requested-With');
608
609
		return $response;
610
	}
611
612
	/**
613
	 * Overloaded redirection logic to trigger a fake redirect on ajax requests.
614
	 * While this violates HTTP principles, its the only way to work around the
615
	 * fact that browsers handle HTTP redirects opaquely, no intervention via JS is possible.
616
	 * In isolation, that's not a problem - but combined with history.pushState()
617
	 * it means we would request the same redirection URL twice if we want to update the URL as well.
618
	 * See LeftAndMain.js for the required jQuery ajaxComplete handlers.
619
	 */
620
	public function redirect($url, $code=302) {
621
		if($this->getRequest()->isAjax()) {
622
			$response = $this->getResponse();
623
			$response->addHeader('X-ControllerURL', $url);
624
			if($this->getRequest()->getHeader('X-Pjax') && !$response->getHeader('X-Pjax')) {
625
				$response->addHeader('X-Pjax', $this->getRequest()->getHeader('X-Pjax'));
626
			}
627
			$newResponse = new LeftAndMain_HTTPResponse(
628
				$response->getBody(),
629
				$response->getStatusCode(),
630
				$response->getStatusDescription()
631
			);
632
			foreach($response->getHeaders() as $k => $v) {
633
				$newResponse->addHeader($k, $v);
634
			}
635
			$newResponse->setIsFinished(true);
636
			$this->setResponse($newResponse);
637
			return ''; // Actual response will be re-requested by client
638
		} else {
639
			parent::redirect($url, $code);
640
		}
641
	}
642
643
	public function index($request) {
644
		return $this->getResponseNegotiator()->respond($request);
645
	}
646
647
	/**
648
	 * If this is set to true, the "switchView" context in the
649
	 * template is shown, with links to the staging and publish site.
650
	 *
651
	 * @return boolean
652
	 */
653
	public function ShowSwitchView() {
654
		return false;
655
	}
656
657
658
	//------------------------------------------------------------------------------------------//
659
	// Main controllers
660
661
	/**
662
	 * You should implement a Link() function in your subclass of LeftAndMain,
663
	 * to point to the URL of that particular controller.
664
	 *
665
	 * @return string
666
	 */
667
	public function Link($action = null) {
668
		// Handle missing url_segments
669
		if($this->config()->url_segment) {
670
			$segment = $this->config()->get('url_segment', Config::FIRST_SET);
671
		} else {
672
			$segment = $this->class;
673
		};
674
675
		$link = Controller::join_links(
676
			$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...
677
			$segment,
678
			'/', // trailing slash needed if $action is null!
679
			"$action"
680
		);
681
		$this->extend('updateLink', $link);
682
		return $link;
683
	}
684
685
	/**
686
	 * @deprecated 5.0
687
	 */
688
	public static function menu_title_for_class($class) {
689
		Deprecation::notice('5.0', 'Use menu_title() instead');
690
		return static::menu_title($class, false);
691
	}
692
693
	/**
694
	 * Get menu title for this section (translated)
695
	 *
696
	 * @param string $class Optional class name if called on LeftAndMain directly
697
	 * @param bool $localise Determine if menu title should be localised via i18n.
698
	 * @return string Menu title for the given class
699
	 */
700
	public static function menu_title($class = null, $localise = true) {
701
		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...
702
			// Respect oveloading of menu_title() in subclasses
703
			return $class::menu_title(null, $localise);
704
		}
705
		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...
706
			$class = get_called_class();
707
		}
708
709
		// Get default class title
710
		$title = Config::inst()->get($class, 'menu_title', Config::FIRST_SET);
711
		if(!$title) {
712
			$title = preg_replace('/Admin$/', '', $class);
713
		}
714
715
		// Check localisation
716
		if(!$localise) {
717
			return $title;
718
		}
719
		return i18n::_t("{$class}.MENUTITLE", $title);
720
	}
721
722
	/**
723
	 * Return styling for the menu icon, if a custom icon is set for this class
724
	 *
725
	 * Example: static $menu-icon = '/path/to/image/';
726
	 * @param string $class
727
	 * @return string
728
	 */
729
	public static function menu_icon_for_class($class) {
730
		$icon = Config::inst()->get($class, 'menu_icon', Config::FIRST_SET);
731
		if (!empty($icon)) {
732
			$class = strtolower(Convert::raw2htmlname(str_replace('\\', '-', $class)));
733
			return ".icon.icon-16.icon-{$class} { background-image: url('{$icon}'); } ";
734
		}
735
		return '';
736
	}
737
738
	public function show($request) {
739
		// TODO Necessary for TableListField URLs to work properly
740
		if($request->param('ID')) $this->setCurrentPageID($request->param('ID'));
741
		return $this->getResponseNegotiator()->respond($request);
742
	}
743
744
	/**
745
	 * Caution: Volatile API.
746
	 *
747
	 * @return PjaxResponseNegotiator
748
	 */
749
	public function getResponseNegotiator() {
750
		if(!$this->responseNegotiator) {
751
			$controller = $this;
752
			$this->responseNegotiator = new PjaxResponseNegotiator(
753
				array(
754
					'CurrentForm' => function() use(&$controller) {
755
						return $controller->getEditForm()->forTemplate();
756
					},
757
					'Content' => function() use(&$controller) {
758
						return $controller->renderWith($controller->getTemplatesWithSuffix('_Content'));
759
					},
760
					'Breadcrumbs' => function() use (&$controller) {
761
						return $controller->renderWith('CMSBreadcrumbs');
762
					},
763
					'default' => function() use(&$controller) {
764
						return $controller->renderWith($controller->getViewer('show'));
765
					}
766
				),
767
				$this->getResponse()
768
			);
769
		}
770
		return $this->responseNegotiator;
771
	}
772
773
	//------------------------------------------------------------------------------------------//
774
	// Main UI components
775
776
	/**
777
	 * Returns the main menu of the CMS.  This is also used by init()
778
	 * to work out which sections the user has access to.
779
	 *
780
	 * @param Boolean
781
	 * @return SS_List
782
	 */
783
	public function MainMenu($cached = true) {
784
		if(!isset($this->_cache_MainMenu) || !$cached) {
785
			// Don't accidentally return a menu if you're not logged in - it's used to determine access.
786
			if(!Member::currentUser()) return new ArrayList();
787
788
			// Encode into DO set
789
			$menu = new ArrayList();
790
			$menuItems = CMSMenu::get_viewable_menu_items();
791
792
			// extra styling for custom menu-icons
793
			$menuIconStyling = '';
794
795
			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...
796
				foreach($menuItems as $code => $menuItem) {
797
					// alternate permission checks (in addition to LeftAndMain->canView())
798
799
					if(
800
						isset($menuItem->controller)
801
						&& $this->hasMethod('alternateMenuDisplayCheck')
802
						&& !$this->alternateMenuDisplayCheck($menuItem->controller)
803
					) {
804
						continue;
805
					}
806
807
					$linkingmode = "link";
808
809
					if($menuItem->controller && get_class($this) == $menuItem->controller) {
810
						$linkingmode = "current";
811
					} else if(strpos($this->Link(), $menuItem->url) !== false) {
812
						if($this->Link() == $menuItem->url) {
813
							$linkingmode = "current";
814
815
						// default menu is the one with a blank {@link url_segment}
816
						} else if(singleton($menuItem->controller)->stat('url_segment') == '') {
817
							if($this->Link() == $this->stat('url_base').'/') {
818
								$linkingmode = "current";
819
							}
820
821
						} else {
822
							$linkingmode = "current";
823
						}
824
					}
825
826
					// already set in CMSMenu::populate_menu(), but from a static pre-controller
827
					// context, so doesn't respect the current user locale in _t() calls - as a workaround,
828
					// we simply call LeftAndMain::menu_title() again
829
					// if we're dealing with a controller
830
					if($menuItem->controller) {
831
						$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...
832
					} else {
833
						$title = $menuItem->title;
834
					}
835
836
					// Provide styling for custom $menu-icon. Done here instead of in
837
					// CMSMenu::populate_menu(), because the icon is part of
838
					// the CMS right pane for the specified class as well...
839
					if($menuItem->controller) {
840
						$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...
841
						if (!empty($menuIcon)) $menuIconStyling .= $menuIcon;
842
					}
843
844
					$menu->push(new ArrayData(array(
845
						"MenuItem" => $menuItem,
846
						"AttributesHTML" => $menuItem->getAttributesHTML(),
847
						"Title" => Convert::raw2xml($title),
848
						"Code" => DBField::create_field('Text', $code),
849
						"Link" => $menuItem->url,
850
						"LinkingMode" => $linkingmode
851
					)));
852
				}
853
			}
854
			if ($menuIconStyling) Requirements::customCSS($menuIconStyling);
855
856
			$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...
857
		}
858
859
		return $this->_cache_MainMenu;
860
	}
861
862
	public function Menu() {
863
		return $this->renderWith($this->getTemplatesWithSuffix('_Menu'));
864
	}
865
866
	/**
867
	 * @todo Wrap in CMSMenu instance accessor
868
	 * @return ArrayData A single menu entry (see {@link MainMenu})
869
	 */
870
	public function MenuCurrentItem() {
871
		$items = $this->MainMenu();
872
		return $items->find('LinkingMode', 'current');
873
	}
874
875
	/**
876
	 * Return a list of appropriate templates for this class, with the given suffix using
877
	 * {@link SSViewer::get_templates_by_class()}
878
	 *
879
	 * @return array
880
	 */
881
	public function getTemplatesWithSuffix($suffix) {
882
		return SSViewer::get_templates_by_class(get_class($this), $suffix, 'LeftAndMain');
883
	}
884
885
	public function Content() {
886
		return $this->renderWith($this->getTemplatesWithSuffix('_Content'));
887
	}
888
889
	public function getRecord($id) {
890
		$className = $this->stat('tree_class');
891
		if($className && $id instanceof $className) {
892
			return $id;
893
		} else if($className && $id == 'root') {
894
			return singleton($className);
895
		} else if($className && is_numeric($id)) {
896
			return DataObject::get_by_id($className, $id);
897
		} else {
898
			return false;
899
		}
900
	}
901
902
	/**
903
	 * @return ArrayList
904
	 */
905
	public function Breadcrumbs($unlinked = false) {
906
		$items = new ArrayList(array(
907
			new ArrayData(array(
908
				'Title' => $this->menu_title(),
909
				'Link' => ($unlinked) ? false : $this->Link()
910
			))
911
		));
912
		$record = $this->currentPage();
913
		if($record && $record->exists()) {
914
			if($record->hasExtension('Hierarchy')) {
915
				$ancestors = $record->getAncestors();
916
				$ancestors = new ArrayList(array_reverse($ancestors->toArray()));
917
				$ancestors->push($record);
918
				foreach($ancestors as $ancestor) {
919
					$items->push(new ArrayData(array(
920
						'Title' => ($ancestor->MenuTitle) ? $ancestor->MenuTitle : $ancestor->Title,
921
						'Link' => ($unlinked) ? false : Controller::join_links($this->Link('show'), $ancestor->ID)
922
					)));
923
				}
924
			} else {
925
				$items->push(new ArrayData(array(
926
					'Title' => ($record->MenuTitle) ? $record->MenuTitle : $record->Title,
927
					'Link' => ($unlinked) ? false : Controller::join_links($this->Link('show'), $record->ID)
928
				)));
929
			}
930
		}
931
932
		return $items;
933
	}
934
935
	/**
936
	 * @return String HTML
937
	 */
938
	public function SiteTreeAsUL() {
939
		$html = $this->getSiteTreeFor($this->stat('tree_class'));
940
		$this->extend('updateSiteTreeAsUL', $html);
941
		return $html;
942
	}
943
944
	/**
945
	 * Gets the current search filter for this request, if available
946
	 *
947
	 * @throws InvalidArgumentException
948
	 * @return LeftAndMain_SearchFilter
949
	 */
950
	protected function getSearchFilter() {
951
		// Check for given FilterClass
952
		$params = $this->getRequest()->getVar('q');
953
		if(empty($params['FilterClass'])) {
954
			return null;
955
		}
956
957
		// Validate classname
958
		$filterClass = $params['FilterClass'];
959
		$filterInfo = new ReflectionClass($filterClass);
960
		if(!$filterInfo->implementsInterface('LeftAndMain_SearchFilter')) {
961
			throw new InvalidArgumentException(sprintf('Invalid filter class passed: %s', $filterClass));
962
		}
963
964
		return Injector::inst()->createWithArgs($filterClass, array($params));
965
	}
966
967
	/**
968
	 * Get a site tree HTML listing which displays the nodes under the given criteria.
969
	 *
970
	 * @param $className The class of the root object
971
	 * @param $rootID The ID of the root object.  If this is null then a complete tree will be
972
	 *  shown
973
	 * @param $childrenMethod The method to call to get the children of the tree. For example,
974
	 *  Children, AllChildrenIncludingDeleted, or AllHistoricalChildren
975
	 * @return String Nested unordered list with links to each page
976
	 */
977
	public function getSiteTreeFor($className, $rootID = null, $childrenMethod = null, $numChildrenMethod = null,
978
			$filterFunction = null, $nodeCountThreshold = 30) {
979
980
		// Filter criteria
981
		$filter = $this->getSearchFilter();
982
983
		// Default childrenMethod and numChildrenMethod
984
		if(!$childrenMethod) $childrenMethod = ($filter && $filter->getChildrenMethod())
985
			? $filter->getChildrenMethod()
986
			: 'AllChildrenIncludingDeleted';
987
988
		if(!$numChildrenMethod) {
989
			$numChildrenMethod = 'numChildren';
990
			if($filter && $filter->getNumChildrenMethod()) {
991
				$numChildrenMethod = $filter->getNumChildrenMethod();
992
			}
993
		}
994
		if(!$filterFunction && $filter) {
995
			$filterFunction = function($node) use($filter) {
996
				return $filter->isPageIncluded($node);
997
			};
998
		}
999
1000
		// Get the tree root
1001
		$record = ($rootID) ? $this->getRecord($rootID) : null;
1002
		$obj = $record ? $record : singleton($className);
1003
1004
		// Get the current page
1005
		// NOTE: This *must* be fetched before markPartialTree() is called, as this
1006
		// causes the Hierarchy::$marked cache to be flushed (@see CMSMain::getRecord)
1007
		// which means that deleted pages stored in the marked tree would be removed
1008
		$currentPage = $this->currentPage();
1009
1010
		// Mark the nodes of the tree to return
1011
		if ($filterFunction) $obj->setMarkingFilterFunction($filterFunction);
1012
1013
		$obj->markPartialTree($nodeCountThreshold, $this, $childrenMethod, $numChildrenMethod);
1014
1015
		// Ensure current page is exposed
1016
		if($currentPage) $obj->markToExpose($currentPage);
1017
1018
		// NOTE: SiteTree/CMSMain coupling :-(
1019
		if(class_exists('SiteTree')) {
1020
			SiteTree::prepopulate_permission_cache('CanEditType', $obj->markedNodeIDs(),
1021
				'SiteTree::can_edit_multiple');
1022
		}
1023
1024
		// getChildrenAsUL is a flexible and complex way of traversing the tree
1025
		$controller = $this;
1026
		$recordController = ($this->stat('tree_class') == 'SiteTree') ?  singleton('CMSPageEditController') : $this;
1027
		$titleFn = function(&$child, $numChildrenMethod) use(&$controller, &$recordController, $filter) {
1028
			$link = Controller::join_links($recordController->Link("show"), $child->ID);
1029
			$node = LeftAndMain_TreeNode::create($child, $link, $controller->isCurrentPage($child), $numChildrenMethod, $filter);
1030
			return $node->forTemplate();
1031
		};
1032
1033
		// Limit the amount of nodes shown for performance reasons.
1034
		// Skip the check if we're filtering the tree, since its not clear how many children will
1035
		// match the filter criteria until they're queried (and matched up with previously marked nodes).
1036
		$nodeThresholdLeaf = Config::inst()->get('Hierarchy', 'node_threshold_leaf');
1037
		if($nodeThresholdLeaf && !$filterFunction) {
1038
			$nodeCountCallback = function($parent, $numChildren) use(&$controller, $className, $nodeThresholdLeaf) {
1039
				if($className == 'SiteTree' && $parent->ID && $numChildren > $nodeThresholdLeaf) {
1040
					return sprintf(
1041
						'<ul><li class="readonly"><span class="item">'
1042
							. '%s (<a href="%s" class="cms-panel-link" data-pjax-target="Content">%s</a>)'
1043
							. '</span></li></ul>',
1044
						_t('LeftAndMain.TooManyPages', 'Too many pages'),
1045
						Controller::join_links(
1046
							$controller->LinkWithSearch($controller->Link()), '
1047
							?view=list&ParentID=' . $parent->ID
1048
						),
1049
						_t(
1050
							'LeftAndMain.ShowAsList',
1051
							'show as list',
1052
							'Show large amount of pages in list instead of tree view'
1053
						)
1054
					);
1055
				}
1056
			};
1057
		} else {
1058
			$nodeCountCallback = null;
1059
		}
1060
1061
		// If the amount of pages exceeds the node thresholds set, use the callback
1062
		$html = null;
1063
		if($obj->ParentID && $nodeCountCallback) {
1064
			$html = $nodeCountCallback($obj, $obj->$numChildrenMethod());
1065
		}
1066
1067
		// Otherwise return the actual tree (which might still filter leaf thresholds on children)
1068
		if(!$html) {
1069
			$html = $obj->getChildrenAsUL(
1070
				"",
1071
				$titleFn,
1072
				singleton('CMSPagesController'),
1073
				true,
1074
				$childrenMethod,
1075
				$numChildrenMethod,
1076
				$nodeCountThreshold,
1077
				$nodeCountCallback
1078
			);
1079
		}
1080
1081
		// Wrap the root if needs be.
1082
		if(!$rootID) {
1083
			$rootLink = $this->Link('show') . '/root';
1084
1085
			// This lets us override the tree title with an extension
1086
			if($this->hasMethod('getCMSTreeTitle') && $customTreeTitle = $this->getCMSTreeTitle()) {
1087
				$treeTitle = $customTreeTitle;
1088
			} elseif(class_exists('SiteConfig')) {
1089
				$siteConfig = SiteConfig::current_site_config();
1090
				$treeTitle =  Convert::raw2xml($siteConfig->Title);
1091
			} else {
1092
				$treeTitle = '...';
1093
			}
1094
1095
			$html = "<ul><li id=\"record-0\" data-id=\"0\" class=\"Root nodelete\"><strong>$treeTitle</strong>"
1096
				. $html . "</li></ul>";
1097
		}
1098
1099
		return $html;
1100
	}
1101
1102
	/**
1103
	 * Get a subtree underneath the request param 'ID'.
1104
	 * If ID = 0, then get the whole tree.
1105
	 */
1106
	public function getsubtree($request) {
1107
		$html = $this->getSiteTreeFor(
1108
			$this->stat('tree_class'),
1109
			$request->getVar('ID'),
1110
			null,
1111
			null,
1112
			null,
1113
			$request->getVar('minNodeCount')
1114
		);
1115
1116
		// Trim off the outer tag
1117
		$html = preg_replace('/^[\s\t\r\n]*<ul[^>]*>/','', $html);
1118
		$html = preg_replace('/<\/ul[^>]*>[\s\t\r\n]*$/','', $html);
1119
1120
		return $html;
1121
	}
1122
1123
	/**
1124
	 * Allows requesting a view update on specific tree nodes.
1125
	 * Similar to {@link getsubtree()}, but doesn't enforce loading
1126
	 * all children with the node. Useful to refresh views after
1127
	 * state modifications, e.g. saving a form.
1128
	 *
1129
	 * @return String JSON
1130
	 */
1131
	public function updatetreenodes($request) {
1132
		$data = array();
1133
		$ids = explode(',', $request->getVar('ids'));
1134
		foreach($ids as $id) {
1135
			if($id === "") continue; // $id may be a blank string, which is invalid and should be skipped over
1136
1137
			$record = $this->getRecord($id);
1138
			if(!$record) continue; // In case a page is no longer available
1139
			$recordController = ($this->stat('tree_class') == 'SiteTree')
1140
				?  singleton('CMSPageEditController')
1141
				: $this;
1142
1143
			// Find the next & previous nodes, for proper positioning (Sort isn't good enough - it's not a raw offset)
1144
			// TODO: These methods should really be in hierarchy - for a start it assumes Sort exists
1145
			$next = $prev = null;
1146
1147
			$className = $this->stat('tree_class');
1148
			$next = DataObject::get($className)
1149
				->filter('ParentID', $record->ParentID)
1150
				->filter('Sort:GreaterThan', $record->Sort)
1151
				->first();
1152
1153
			if (!$next) {
1154
				$prev = DataObject::get($className)
1155
					->filter('ParentID', $record->ParentID)
1156
					->filter('Sort:LessThan', $record->Sort)
1157
					->reverse()
1158
					->first();
1159
			}
1160
1161
			$link = Controller::join_links($recordController->Link("show"), $record->ID);
1162
			$html = LeftAndMain_TreeNode::create($record, $link, $this->isCurrentPage($record))
1163
				->forTemplate() . '</li>';
1164
1165
			$data[$id] = array(
1166
				'html' => $html,
1167
				'ParentID' => $record->ParentID,
1168
				'NextID' => $next ? $next->ID : null,
1169
				'PrevID' => $prev ? $prev->ID : null
1170
			);
1171
		}
1172
		$this->getResponse()->addHeader('Content-Type', 'text/json');
1173
		return Convert::raw2json($data);
1174
	}
1175
1176
	/**
1177
	 * Save  handler
1178
	 *
1179
	 * @param array $data
1180
	 * @param Form $form
1181
	 * @return SS_HTTPResponse
1182
	 */
1183
	public function save($data, $form) {
1184
		$request = $this->getRequest();
1185
		$className = $this->stat('tree_class');
1186
1187
		// Existing or new record?
1188
		$id = $data['ID'];
1189
		if(is_numeric($id) && $id > 0) {
1190
			$record = DataObject::get_by_id($className, $id);
1191
			if($record && !$record->canEdit()) {
1192
				return Security::permissionFailure($this);
1193
			}
1194
			if(!$record || !$record->ID) {
1195
				$this->httpError(404, "Bad record ID #" . (int)$id);
1196
			}
1197
		} else {
1198
			if(!singleton($this->stat('tree_class'))->canCreate()) {
1199
				return Security::permissionFailure($this);
1200
			}
1201
			$record = $this->getNewItem($id, false);
1202
		}
1203
1204
		// save form data into record
1205
		$form->saveInto($record, true);
1206
		$record->write();
1207
		$this->extend('onAfterSave', $record);
1208
		$this->setCurrentPageID($record->ID);
1209
1210
		$message = _t('LeftAndMain.SAVEDUP', 'Saved.');
1211
		if($request->getHeader('X-Formschema-Request')) {
1212
			$form->setMessage($message, 'good');
1213
			$data = $this->getSchemaForForm($form);
1214
			$response = new SS_HTTPResponse(Convert::raw2json($data));
1215
			$response->addHeader('Content-Type', 'application/json');
1216
		} else {
1217
			$response = $this->getResponseNegotiator()->respond($request);
1218
		}
1219
1220
		$response->addHeader('X-Status', rawurlencode($message));
1221
		return $response;
1222
	}
1223
1224
	/**
1225
	 * Create new item.
1226
	 *
1227
	 * @param string|int $id
1228
	 * @param bool $setID
1229
	 * @return DataObject
1230
	 */
1231
	public function getNewItem($id, $setID = true) {
1232
		$class = $this->stat('tree_class');
1233
		$object = Injector::inst()->create($class);
1234
		if($setID) {
1235
			$object->ID = $id;
1236
		}
1237
		return $object;
1238
	}
1239
1240
	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...
1241
		$className = $this->stat('tree_class');
1242
1243
		$id = $data['ID'];
1244
		$record = DataObject::get_by_id($className, $id);
1245
		if($record && !$record->canDelete()) return Security::permissionFailure();
1246
		if(!$record || !$record->ID) $this->httpError(404, "Bad record ID #" . (int)$id);
1247
1248
		$record->delete();
1249
1250
		$this->getResponse()->addHeader('X-Status', rawurlencode(_t('LeftAndMain.DELETED', 'Deleted.')));
1251
		return $this->getResponseNegotiator()->respond(
1252
			$this->getRequest(),
1253
			array('currentform' => array($this, 'EmptyForm'))
1254
		);
1255
	}
1256
1257
	/**
1258
	 * Update the position and parent of a tree node.
1259
	 * Only saves the node if changes were made.
1260
	 *
1261
	 * Required data:
1262
	 * - 'ID': The moved node
1263
	 * - 'ParentID': New parent relation of the moved node (0 for root)
1264
	 * - 'SiblingIDs': Array of all sibling nodes to the moved node (incl. the node itself).
1265
	 *   In case of a 'ParentID' change, relates to the new siblings under the new parent.
1266
	 *
1267
	 * @return SS_HTTPResponse JSON string with a
1268
	 */
1269
	public function savetreenode($request) {
1270
		if (!Permission::check('SITETREE_REORGANISE') && !Permission::check('ADMIN')) {
1271
			$this->getResponse()->setStatusCode(
1272
				403,
1273
				_t('LeftAndMain.CANT_REORGANISE',
1274
					"You do not have permission to rearange the site tree. Your change was not saved.")
1275
			);
1276
			return;
1277
		}
1278
1279
		$className = $this->stat('tree_class');
1280
		$statusUpdates = array('modified'=>array());
1281
		$id = $request->requestVar('ID');
1282
		$parentID = $request->requestVar('ParentID');
1283
1284
		if($className == 'SiteTree' && $page = DataObject::get_by_id('Page', $id)){
1285
			$root = $page->getParentType();
1286
			if(($parentID == '0' || $root == 'root') && !SiteConfig::current_site_config()->canCreateTopLevel()){
1287
				$this->getResponse()->setStatusCode(
1288
					403,
1289
					_t('LeftAndMain.CANT_REORGANISE',
1290
						"You do not have permission to alter Top level pages. Your change was not saved.")
1291
					);
1292
				return;
1293
			}
1294
		}
1295
1296
		$siblingIDs = $request->requestVar('SiblingIDs');
1297
		$statusUpdates = array('modified'=>array());
1298
		if(!is_numeric($id) || !is_numeric($parentID)) throw new InvalidArgumentException();
1299
1300
		$node = DataObject::get_by_id($className, $id);
1301
		if($node && !$node->canEdit()) return Security::permissionFailure($this);
1302
1303
		if(!$node) {
1304
			$this->getResponse()->setStatusCode(
1305
				500,
1306
				_t('LeftAndMain.PLEASESAVE',
1307
					"Please Save Page: This page could not be updated because it hasn't been saved yet."
1308
				)
1309
			);
1310
			return;
1311
		}
1312
1313
		// Update hierarchy (only if ParentID changed)
1314
		if($node->ParentID != $parentID) {
1315
			$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...
1316
			$node->write();
1317
1318
			$statusUpdates['modified'][$node->ID] = array(
1319
				'TreeTitle'=>$node->TreeTitle
1320
			);
1321
1322
			// Update all dependent pages
1323
			if(class_exists('VirtualPage')) {
1324
				$virtualPages = VirtualPage::get()->filter("CopyContentFromID", $node->ID);
1325
				foreach($virtualPages as $virtualPage) {
1326
					$statusUpdates['modified'][$virtualPage->ID] = array(
1327
						'TreeTitle' => $virtualPage->TreeTitle()
1328
					);
1329
				}
1330
			}
1331
1332
			$this->getResponse()->addHeader('X-Status',
1333
				rawurlencode(_t('LeftAndMain.REORGANISATIONSUCCESSFUL', 'Reorganised the site tree successfully.')));
1334
		}
1335
1336
		// Update sorting
1337
		if(is_array($siblingIDs)) {
1338
			$counter = 0;
1339
			foreach($siblingIDs as $id) {
1340
				if($id == $node->ID) {
1341
					$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...
1342
					$node->write();
1343
					$statusUpdates['modified'][$node->ID] = array(
1344
						'TreeTitle' => $node->TreeTitle
1345
					);
1346
				} else if(is_numeric($id)) {
1347
					// Nodes that weren't "actually moved" shouldn't be registered as
1348
					// having been edited; do a direct SQL update instead
1349
					++$counter;
1350
					DB::prepared_query(
1351
						"UPDATE \"$className\" SET \"Sort\" = ? WHERE \"ID\" = ?",
1352
						array($counter, $id)
1353
					);
1354
				}
1355
			}
1356
1357
			$this->getResponse()->addHeader('X-Status',
1358
				rawurlencode(_t('LeftAndMain.REORGANISATIONSUCCESSFUL', 'Reorganised the site tree successfully.')));
1359
		}
1360
1361
		return Convert::raw2json($statusUpdates);
1362
	}
1363
1364
	public function CanOrganiseSitetree() {
1365
		return !Permission::check('SITETREE_REORGANISE') && !Permission::check('ADMIN') ? false : true;
1366
	}
1367
1368
	/**
1369
	 * Retrieves an edit form, either for display, or to process submitted data.
1370
	 * Also used in the template rendered through {@link Right()} in the $EditForm placeholder.
1371
	 *
1372
	 * This is a "pseudo-abstract" methoed, usually connected to a {@link getEditForm()}
1373
	 * method in an entwine subclass. This method can accept a record identifier,
1374
	 * selected either in custom logic, or through {@link currentPageID()}.
1375
	 * The form usually construct itself from {@link DataObject->getCMSFields()}
1376
	 * for the specific managed subclass defined in {@link LeftAndMain::$tree_class}.
1377
	 *
1378
	 * @param HTTPRequest $request Optionally contains an identifier for the
1379
	 *  record to load into the form.
1380
	 * @return Form Should return a form regardless wether a record has been found.
1381
	 *  Form might be readonly if the current user doesn't have the permission to edit
1382
	 *  the record.
1383
	 */
1384
	/**
1385
	 * @return Form
1386
	 */
1387
	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...
1388
		return $this->getEditForm();
1389
	}
1390
1391
	/**
1392
	 * Calls {@link SiteTree->getCMSFields()}
1393
	 *
1394
	 * @param Int $id
1395
	 * @param FieldList $fields
1396
	 * @return Form
1397
	 */
1398
	public function getEditForm($id = null, $fields = null) {
1399
		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...
1400
1401
		if(is_object($id)) {
1402
			$record = $id;
1403
		} else {
1404
			$record = $this->getRecord($id);
1405
			if($record && !$record->canView()) return Security::permissionFailure($this);
1406
		}
1407
1408
		if($record) {
1409
			$fields = ($fields) ? $fields : $record->getCMSFields();
1410
			if ($fields == null) {
1411
				user_error(
1412
					"getCMSFields() returned null  - it should return a FieldList object.
1413
					Perhaps you forgot to put a return statement at the end of your method?",
1414
					E_USER_ERROR
1415
				);
1416
			}
1417
1418
			// Add hidden fields which are required for saving the record
1419
			// and loading the UI state
1420
			if(!$fields->dataFieldByName('ClassName')) {
1421
				$fields->push(new HiddenField('ClassName'));
1422
			}
1423
1424
			$tree_class = $this->stat('tree_class');
1425
			if(
1426
				$tree_class::has_extension('Hierarchy')
1427
				&& !$fields->dataFieldByName('ParentID')
1428
			) {
1429
				$fields->push(new HiddenField('ParentID'));
1430
			}
1431
1432
			// Added in-line to the form, but plucked into different view by frontend scripts.
1433
			if(in_array('CMSPreviewable', class_implements($record))) {
1434
				$navField = new LiteralField('SilverStripeNavigator', $this->getSilverStripeNavigator());
1435
				$navField->setAllowHTML(true);
1436
				$fields->push($navField);
1437
			}
1438
1439
			if($record->hasMethod('getAllCMSActions')) {
1440
				$actions = $record->getAllCMSActions();
1441
			} else {
1442
				$actions = $record->getCMSActions();
1443
				// add default actions if none are defined
1444
				if(!$actions || !$actions->Count()) {
1445
					if($record->hasMethod('canEdit') && $record->canEdit()) {
1446
						$actions->push(
1447
							FormAction::create('save',_t('CMSMain.SAVE','Save'))
1448
								->addExtraClass('ss-ui-action-constructive')->setAttribute('data-icon', 'accept')
1449
						);
1450
					}
1451
					if($record->hasMethod('canDelete') && $record->canDelete()) {
1452
						$actions->push(
1453
							FormAction::create('delete',_t('ModelAdmin.DELETE','Delete'))
1454
								->addExtraClass('ss-ui-action-destructive')
1455
						);
1456
					}
1457
				}
1458
			}
1459
1460
			// Use <button> to allow full jQuery UI styling
1461
			$actionsFlattened = $actions->dataFields();
1462
			if($actionsFlattened) foreach($actionsFlattened as $action) $action->setUseButtonTag(true);
1463
1464
			$negotiator = $this->getResponseNegotiator();
1465
			$form = Form::create(
1466
				$this, "EditForm", $fields, $actions
1467
			)->setHTMLID('Form_EditForm');
1468
			$form->addExtraClass('cms-edit-form');
1469
			$form->loadDataFrom($record);
1470
			$form->setTemplate($this->getTemplatesWithSuffix('_EditForm'));
1471
			$form->setAttribute('data-pjax-fragment', 'CurrentForm');
1472
			$form->setValidationResponseCallback(function() use ($negotiator, $form) {
1473
				$request = $this->getRequest();
1474
				if($request->isAjax() && $negotiator) {
1475
					$form->setupFormErrors();
1476
					$result = $form->forTemplate();
1477
1478
					return $negotiator->respond($request, array(
1479
						'CurrentForm' => function() use($result) {
1480
							return $result;
1481
						}
1482
					));
1483
				}
1484
			});
1485
1486
			// Announce the capability so the frontend can decide whether to allow preview or not.
1487
			if(in_array('CMSPreviewable', class_implements($record))) {
1488
				$form->addExtraClass('cms-previewable');
1489
			}
1490
1491
			// Set this if you want to split up tabs into a separate header row
1492
			// 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...
1493
			// 	$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...
1494
			// }
1495
1496
			// Add a default or custom validator.
1497
			// @todo Currently the default Validator.js implementation
1498
			//  adds javascript to the document body, meaning it won't
1499
			//  be included properly if the associated fields are loaded
1500
			//  through ajax. This means only serverside validation
1501
			//  will kick in for pages+validation loaded through ajax.
1502
			//  This will be solved by using less obtrusive javascript validation
1503
			//  in the future, see http://open.silverstripe.com/ticket/2915 and
1504
			//  http://open.silverstripe.com/ticket/3386
1505
			if($record->hasMethod('getCMSValidator')) {
1506
				$validator = $record->getCMSValidator();
1507
				// The clientside (mainly LeftAndMain*.js) rely on ajax responses
1508
				// which can be evaluated as javascript, hence we need
1509
				// to override any global changes to the validation handler.
1510
				if($validator != NULL){
1511
					$form->setValidator($validator);
1512
				}
1513
			} else {
1514
				$form->unsetValidator();
1515
			}
1516
1517
			if($record->hasMethod('canEdit') && !$record->canEdit()) {
1518
				$readonlyFields = $form->Fields()->makeReadonly();
1519
				$form->setFields($readonlyFields);
1520
			}
1521
		} else {
1522
			$form = $this->EmptyForm();
1523
		}
1524
1525
		return $form;
1526
	}
1527
1528
	/**
1529
	 * Returns a placeholder form, used by {@link getEditForm()} if no record is selected.
1530
	 * Our javascript logic always requires a form to be present in the CMS interface.
1531
	 *
1532
	 * @return Form
1533
	 */
1534
	public function EmptyForm() {
1535
		$form = Form::create(
1536
			$this,
1537
			"EditForm",
1538
			new FieldList(
1539
				// new HeaderField(
1540
				// 	'WelcomeHeader',
1541
				// 	$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...
1542
				// ),
1543
				// new LiteralField(
1544
				// 	'WelcomeText',
1545
				// 	sprintf('<p id="WelcomeMessage">%s %s. %s</p>',
1546
				// 		_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...
1547
				// 		$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...
1548
				// 		_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...
1549
				// 	)
1550
				// )
1551
			),
1552
			new FieldList()
1553
		)->setHTMLID('Form_EditForm');
1554
		$form->unsetValidator();
1555
		$form->addExtraClass('cms-edit-form');
1556
		$form->addExtraClass('root-form');
1557
		$form->setTemplate($this->getTemplatesWithSuffix('_EditForm'));
1558
		$form->setAttribute('data-pjax-fragment', 'CurrentForm');
1559
1560
		return $form;
1561
	}
1562
1563
	/**
1564
	 * Return the CMS's HTML-editor toolbar
1565
	 */
1566
	public function EditorToolbar() {
1567
		return HtmlEditorField_Toolbar::create($this, "EditorToolbar");
1568
	}
1569
1570
	/**
1571
	 * Renders a panel containing tools which apply to all displayed
1572
	 * "content" (mostly through {@link EditForm()}), for example a tree navigation or a filter panel.
1573
	 * Auto-detects applicable templates by naming convention: "<controller classname>_Tools.ss",
1574
	 * and takes the most specific template (see {@link getTemplatesWithSuffix()}).
1575
	 * To explicitly disable the panel in the subclass, simply create a more specific, empty template.
1576
	 *
1577
	 * @return String HTML
1578
	 */
1579
	public function Tools() {
1580
		$templates = $this->getTemplatesWithSuffix('_Tools');
1581
		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...
1582
			$viewer = new SSViewer($templates);
1583
			return $viewer->process($this);
1584
		} else {
1585
			return false;
1586
		}
1587
	}
1588
1589
	/**
1590
	 * Renders a panel containing tools which apply to the currently displayed edit form.
1591
	 * The main difference to {@link Tools()} is that the panel is displayed within
1592
	 * the element structure of the form panel (rendered through {@link EditForm}).
1593
	 * This means the panel will be loaded alongside new forms, and refreshed upon save,
1594
	 * which can mean a performance hit, depending on how complex your panel logic gets.
1595
	 * Any form fields contained in the returned markup will also be submitted with the main form,
1596
	 * which might be desired depending on the implementation details.
1597
	 *
1598
	 * @return String HTML
1599
	 */
1600
	public function EditFormTools() {
1601
		$templates = $this->getTemplatesWithSuffix('_EditFormTools');
1602
		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...
1603
			$viewer = new SSViewer($templates);
1604
			return $viewer->process($this);
1605
		} else {
1606
			return false;
1607
		}
1608
	}
1609
1610
	/**
1611
	 * Batch Actions Handler
1612
	 */
1613
	public function batchactions() {
1614
		return new CMSBatchActionHandler($this, 'batchactions', $this->stat('tree_class'));
1615
	}
1616
1617
	/**
1618
	 * @return Form
1619
	 */
1620
	public function BatchActionsForm() {
1621
		$actions = $this->batchactions()->batchActionList();
1622
		$actionsMap = array('-1' => _t('LeftAndMain.DropdownBatchActionsDefault', 'Choose an action...')); // Placeholder action
1623
		foreach($actions as $action) {
1624
			$actionsMap[$action->Link] = $action->Title;
1625
		}
1626
1627
		$form = new Form(
1628
			$this,
1629
			'BatchActionsForm',
1630
			new FieldList(
1631
				new HiddenField('csvIDs'),
1632
				DropdownField::create(
1633
					'Action',
1634
					false,
1635
					$actionsMap
1636
				)
1637
					->setAttribute('autocomplete', 'off')
1638
					->setAttribute('data-placeholder', _t('LeftAndMain.DropdownBatchActionsDefault', 'Choose an action...'))
1639
			),
1640
			new FieldList(
1641
				// TODO i18n
1642
				new FormAction('submit', _t('Form.SubmitBtnLabel', "Go"))
1643
			)
1644
		);
1645
		$form->addExtraClass('cms-batch-actions nostyle');
1646
		$form->unsetValidator();
1647
1648
		$this->extend('updateBatchActionsForm', $form);
1649
		return $form;
1650
	}
1651
1652
	public function printable() {
1653
		$form = $this->getEditForm($this->currentPageID());
1654
		if(!$form) return false;
1655
1656
		$form->transform(new PrintableTransformation());
1657
		$form->setActions(null);
1658
1659
		Requirements::clear();
1660
		Requirements::css(FRAMEWORK_ADMIN_DIR . '/dist/css/LeftAndMain_printable.css');
1661
		return array(
1662
			"PrintForm" => $form
1663
		);
1664
	}
1665
1666
	/**
1667
	 * Used for preview controls, mainly links which switch between different states of the page.
1668
	 *
1669
	 * @return ArrayData
1670
	 */
1671
	public function getSilverStripeNavigator() {
1672
		$page = $this->currentPage();
1673
		if($page) {
1674
			$navigator = new SilverStripeNavigator($page);
1675
			return $navigator->renderWith($this->getTemplatesWithSuffix('_SilverStripeNavigator'));
1676
		} else {
1677
			return false;
1678
		}
1679
	}
1680
1681
	/**
1682
	 * Identifier for the currently shown record,
1683
	 * in most cases a database ID. Inspects the following
1684
	 * sources (in this order):
1685
	 * - GET/POST parameter named 'ID'
1686
	 * - URL parameter named 'ID'
1687
	 * - Session value namespaced by classname, e.g. "CMSMain.currentPage"
1688
	 *
1689
	 * @return int
1690
	 */
1691
	public function currentPageID() {
1692
		if($this->getRequest()->requestVar('ID') && is_numeric($this->getRequest()->requestVar('ID')))	{
1693
			return $this->getRequest()->requestVar('ID');
1694
		} elseif (isset($this->urlParams['ID']) && is_numeric($this->urlParams['ID'])) {
1695
			return $this->urlParams['ID'];
1696
		} elseif(Session::get($this->sessionNamespace() . ".currentPage")) {
1697
			return Session::get($this->sessionNamespace() . ".currentPage");
1698
		} else {
1699
			return null;
1700
		}
1701
	}
1702
1703
	/**
1704
	 * Forces the current page to be set in session,
1705
	 * which can be retrieved later through {@link currentPageID()}.
1706
	 * Keep in mind that setting an ID through GET/POST or
1707
	 * as a URL parameter will overrule this value.
1708
	 *
1709
	 * @param int $id
1710
	 */
1711
	public function setCurrentPageID($id) {
1712
		Session::set($this->sessionNamespace() . ".currentPage", $id);
1713
	}
1714
1715
	/**
1716
	 * Uses {@link getRecord()} and {@link currentPageID()}
1717
	 * to get the currently selected record.
1718
	 *
1719
	 * @return DataObject
1720
	 */
1721
	public function currentPage() {
1722
		return $this->getRecord($this->currentPageID());
1723
	}
1724
1725
	/**
1726
	 * Compares a given record to the currently selected one (if any).
1727
	 * Used for marking the current tree node.
1728
	 *
1729
	 * @return boolean
1730
	 */
1731
	public function isCurrentPage(DataObject $record) {
1732
		return ($record->ID == $this->currentPageID());
1733
	}
1734
1735
	/**
1736
	 * @return String
1737
	 */
1738
	protected function sessionNamespace() {
1739
		$override = $this->stat('session_namespace');
1740
		return $override ? $override : $this->class;
1741
	}
1742
1743
	/**
1744
	 * URL to a previewable record which is shown through this controller.
1745
	 * The controller might not have any previewable content, in which case
1746
	 * this method returns FALSE.
1747
	 *
1748
	 * @return String|boolean
1749
	 */
1750
	public function LinkPreview() {
1751
		return false;
1752
	}
1753
1754
	/**
1755
	 * Return the version number of this application.
1756
	 * Uses the number in <mymodule>/silverstripe_version
1757
	 * (automatically replaced by build scripts).
1758
	 * If silverstripe_version is empty,
1759
	 * then attempts to get it from composer.lock
1760
	 *
1761
	 * @return string
1762
	 */
1763
	public function CMSVersion() {
1764
		$versions = array();
1765
		$modules = array(
1766
			'silverstripe/framework' => array(
1767
				'title' => 'Framework',
1768
				'versionFile' => FRAMEWORK_PATH . '/silverstripe_version',
1769
			)
1770
		);
1771
		if(defined('CMS_PATH')) {
1772
			$modules['silverstripe/cms'] = array(
1773
				'title' => 'CMS',
1774
				'versionFile' => CMS_PATH . '/silverstripe_version',
1775
			);
1776
		}
1777
1778
		// Tries to obtain version number from composer.lock if it exists
1779
		$composerLockPath = BASE_PATH . '/composer.lock';
1780
		if (file_exists($composerLockPath)) {
1781
			$cache = SS_Cache::factory('LeftAndMain_CMSVersion');
1782
			$cacheKey = filemtime($composerLockPath);
1783
			$versions = $cache->load($cacheKey);
1784
			if($versions) {
1785
				$versions = json_decode($versions, true);
1786
			} else {
1787
				$versions = array();
1788
			}
1789
			if(!$versions && $jsonData = file_get_contents($composerLockPath)) {
1790
				$lockData = json_decode($jsonData);
1791
				if($lockData && isset($lockData->packages)) {
1792
					foreach ($lockData->packages as $package) {
1793
						if(
1794
							array_key_exists($package->name, $modules)
1795
							&& isset($package->version)
1796
						) {
1797
							$versions[$package->name] = $package->version;
1798
						}
1799
					}
1800
					$cache->save(json_encode($versions), $cacheKey);
1801
				}
1802
			}
1803
		}
1804
1805
		// Fall back to static version file
1806
		foreach($modules as $moduleName => $moduleSpec) {
1807
			if(!isset($versions[$moduleName])) {
1808
				if($staticVersion = file_get_contents($moduleSpec['versionFile'])) {
1809
					$versions[$moduleName] = $staticVersion;
1810
				} else {
1811
					$versions[$moduleName] = _t('LeftAndMain.VersionUnknown', 'Unknown');
1812
				}
1813
			}
1814
		}
1815
1816
		$out = array();
1817
		foreach($modules as $moduleName => $moduleSpec) {
1818
			$out[] = $modules[$moduleName]['title'] . ': ' . $versions[$moduleName];
1819
		}
1820
		return implode(', ', $out);
1821
	}
1822
1823
	/**
1824
	 * @return array
1825
	 */
1826
	public function SwitchView() {
1827
		if($page = $this->currentPage()) {
1828
			$nav = SilverStripeNavigator::get_for_record($page);
1829
			return $nav['items'];
1830
		}
1831
	}
1832
1833
	/**
1834
	 * @return SiteConfig
1835
	 */
1836
	public function SiteConfig() {
1837
		return (class_exists('SiteConfig')) ? SiteConfig::current_site_config() : null;
1838
	}
1839
1840
	/**
1841
	 * The href for the anchor on the Silverstripe logo.
1842
	 * Set by calling LeftAndMain::set_application_link()
1843
	 *
1844
	 * @config
1845
	 * @var String
1846
	 */
1847
	private static $application_link = '//www.silverstripe.org/';
1848
1849
	/**
1850
	 * Sets the href for the anchor on the Silverstripe logo in the menu
1851
	 *
1852
	 * @deprecated since version 4.0
1853
	 *
1854
	 * @param String $link
1855
	 */
1856
	public static function set_application_link($link) {
1857
		Deprecation::notice('4.0', 'Use the "LeftAndMain.application_link" config setting instead');
1858
		Config::inst()->update('LeftAndMain', 'application_link', $link);
1859
	}
1860
1861
	/**
1862
	 * @return String
1863
	 */
1864
	public function ApplicationLink() {
1865
		return $this->stat('application_link');
1866
	}
1867
1868
	/**
1869
	 * The application name. Customisable by calling
1870
	 * LeftAndMain::setApplicationName() - the first parameter.
1871
	 *
1872
	 * @config
1873
	 * @var String
1874
	 */
1875
	private static $application_name = 'SilverStripe';
1876
1877
	/**
1878
	 * @param String $name
1879
	 * @deprecated since version 4.0
1880
	 */
1881
	public static function setApplicationName($name) {
1882
		Deprecation::notice('4.0', 'Use the "LeftAndMain.application_name" config setting instead');
1883
		Config::inst()->update('LeftAndMain', 'application_name', $name);
1884
	}
1885
1886
	/**
1887
	 * Get the application name.
1888
	 *
1889
	 * @return string
1890
	 */
1891
	public function getApplicationName() {
1892
		return $this->stat('application_name');
1893
	}
1894
1895
	/**
1896
	 * @return string
1897
	 */
1898
	public function Title() {
1899
		$app = $this->getApplicationName();
1900
1901
		return ($section = $this->SectionTitle()) ? sprintf('%s - %s', $app, $section) : $app;
1902
	}
1903
1904
	/**
1905
	 * Return the title of the current section. Either this is pulled from
1906
	 * the current panel's menu_title or from the first active menu
1907
	 *
1908
	 * @return string
1909
	 */
1910
	public function SectionTitle() {
1911
		$title = $this->menu_title();
1912
		if($title) {
1913
			return $title;
1914
		}
1915
1916
		foreach($this->MainMenu() as $menuItem) {
1917
			if($menuItem->LinkingMode != 'link') {
1918
				return $menuItem->Title;
1919
			}
1920
		}
1921
	}
1922
1923
	/**
1924
	 * Same as {@link ViewableData->CSSClasses()}, but with a changed name
1925
	 * to avoid problems when using {@link ViewableData->customise()}
1926
	 * (which always returns "ArrayData" from the $original object).
1927
	 *
1928
	 * @return String
1929
	 */
1930
	public function BaseCSSClasses() {
1931
		return $this->CSSClasses('Controller');
1932
	}
1933
1934
	/**
1935
	 * @return String
1936
	 */
1937
	public function Locale() {
1938
		return DBField::create_field('Locale', i18n::get_locale());
1939
	}
1940
1941
	public function providePermissions() {
1942
		$perms = array(
1943
			"CMS_ACCESS_LeftAndMain" => array(
1944
				'name' => _t('CMSMain.ACCESSALLINTERFACES', 'Access to all CMS sections'),
1945
				'category' => _t('Permission.CMS_ACCESS_CATEGORY', 'CMS Access'),
1946
				'help' => _t('CMSMain.ACCESSALLINTERFACESHELP', 'Overrules more specific access settings.'),
1947
				'sort' => -100
1948
			)
1949
		);
1950
1951
		// Add any custom ModelAdmin subclasses. Can't put this on ModelAdmin itself
1952
		// since its marked abstract, and needs to be singleton instanciated.
1953
		foreach(ClassInfo::subclassesFor('ModelAdmin') as $i => $class) {
0 ignored issues
show
Bug introduced by
The expression \ClassInfo::subclassesFor('ModelAdmin') of type null|array is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

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

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

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

Loading history...
1954
			if($class == 'ModelAdmin') continue;
1955
			if(ClassInfo::classImplements($class, 'TestOnly')) continue;
1956
1957
			$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...
1958
			$perms["CMS_ACCESS_" . $class] = array(
1959
				'name' => _t(
1960
					'CMSMain.ACCESS',
1961
					"Access to '{title}' section",
1962
					"Item in permission selection identifying the admin section. Example: Access to 'Files & Images'",
1963
					array('title' => $title)
1964
				),
1965
				'category' => _t('Permission.CMS_ACCESS_CATEGORY', 'CMS Access')
1966
			);
1967
		}
1968
1969
		return $perms;
1970
	}
1971
1972
	/**
1973
	 * Register the given javascript file as required in the CMS.
1974
	 * Filenames should be relative to the base, eg, FRAMEWORK_DIR . '/client/dist/js/loader.js'
1975
	 *
1976
	 * @deprecated since version 4.0
1977
	 */
1978
	public static function require_javascript($file) {
1979
		Deprecation::notice('4.0', 'Use "LeftAndMain.extra_requirements_javascript" config setting instead');
1980
		Config::inst()->update('LeftAndMain', 'extra_requirements_javascript', array($file => array()));
1981
	}
1982
1983
	/**
1984
	 * Register the given stylesheet file as required.
1985
	 * @deprecated since version 4.0
1986
	 *
1987
	 * @param $file String Filenames should be relative to the base, eg, THIRDPARTY_DIR . '/tree/tree.css'
1988
	 * @param $media String Comma-separated list of media-types (e.g. "screen,projector")
1989
	 * @see http://www.w3.org/TR/REC-CSS2/media.html
1990
	 */
1991
	public static function require_css($file, $media = null) {
1992
		Deprecation::notice('4.0', 'Use "LeftAndMain.extra_requirements_css" config setting instead');
1993
		Config::inst()->update('LeftAndMain', 'extra_requirements_css', array($file => array('media' => $media)));
1994
	}
1995
1996
	/**
1997
	 * Register the given "themeable stylesheet" as required.
1998
	 * Themeable stylesheets have globally unique names, just like templates and PHP files.
1999
	 * Because of this, they can be replaced by similarly named CSS files in the theme directory.
2000
	 *
2001
	 * @deprecated since version 4.0
2002
	 *
2003
	 * @param $name String The identifier of the file.  For example, css/MyFile.css would have the identifier "MyFile"
2004
	 * @param $media String Comma-separated list of media-types (e.g. "screen,projector")
2005
	 */
2006
	public static function require_themed_css($name, $media = null) {
2007
		Deprecation::notice('4.0', 'Use "LeftAndMain.extra_requirements_themedCss" config setting instead');
2008
		Config::inst()->update('LeftAndMain', 'extra_requirements_themedCss', array($name => array('media' => $media)));
2009
	}
2010
2011
}
2012
2013
/**
2014
 * @package cms
2015
 * @subpackage core
2016
 */
2017
class LeftAndMainMarkingFilter {
2018
2019
	/**
2020
	 * @var array Request params (unsanitized)
2021
	 */
2022
	protected $params = array();
2023
2024
	/**
2025
	 * @param array $params Request params (unsanitized)
2026
	 */
2027
	public function __construct($params = null) {
2028
		$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...
2029
		$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...
2030
		$parents = array();
2031
2032
		$q = $this->getQuery($params);
2033
		$res = $q->execute();
2034
		if (!$res) return;
2035
2036
		// And keep a record of parents we don't need to get parents
2037
		// of themselves, as well as IDs to mark
2038
		foreach($res as $row) {
2039
			if ($row['ParentID']) $parents[$row['ParentID']] = true;
2040
			$this->ids[$row['ID']] = true;
2041
		}
2042
2043
		// We need to recurse up the tree,
2044
		// finding ParentIDs for each ID until we run out of parents
2045
		while (!empty($parents)) {
2046
			$parentsClause = DB::placeholders($parents);
2047
			$res = DB::prepared_query(
2048
				"SELECT \"ParentID\", \"ID\" FROM \"SiteTree\" WHERE \"ID\" in ($parentsClause)",
2049
				array_keys($parents)
2050
			);
2051
			$parents = array();
2052
2053
			foreach($res as $row) {
2054
				if ($row['ParentID']) $parents[$row['ParentID']] = true;
2055
				$this->ids[$row['ID']] = true;
2056
				$this->expanded[$row['ID']] = true;
2057
			}
2058
		}
2059
	}
2060
2061
	protected function getQuery($params) {
2062
		$where = array();
2063
2064
		if(isset($params['ID'])) unset($params['ID']);
2065
		if($treeClass = static::config()->tree_class) foreach($params as $name => $val) {
2066
			// Partial string match against a variety of fields
2067
			if(!empty($val) && singleton($treeClass)->hasDatabaseField($name)) {
2068
				$predicate = sprintf('"%s" LIKE ?', $name);
2069
				$where[$predicate] = "%$val%";
2070
			}
2071
		}
2072
2073
		return new SQLSelect(
2074
			array("ParentID", "ID"),
2075
			'SiteTree',
2076
			$where
2077
		);
2078
	}
2079
2080
	public function mark($node) {
2081
		$id = $node->ID;
2082
		if(array_key_exists((int) $id, $this->expanded)) $node->markOpened();
2083
		return array_key_exists((int) $id, $this->ids) ? $this->ids[$id] : false;
2084
	}
2085
}
2086
2087
/**
2088
 * Allow overriding finished state for faux redirects.
2089
 *
2090
 * @package framework
2091
 * @subpackage admin
2092
 */
2093
class LeftAndMain_HTTPResponse extends SS_HTTPResponse {
2094
2095
	protected $isFinished = false;
2096
2097
	public function isFinished() {
2098
		return (parent::isFinished() || $this->isFinished);
2099
	}
2100
2101
	public function setIsFinished($bool) {
2102
		$this->isFinished = $bool;
2103
	}
2104
2105
}
2106
2107
/**
2108
 * Wrapper around objects being displayed in a tree.
2109
 * Caution: Volatile API.
2110
 *
2111
 * @todo Implement recursive tree node rendering.
2112
 *
2113
 * @package framework
2114
 * @subpackage admin
2115
 */
2116
class LeftAndMain_TreeNode extends ViewableData {
2117
2118
	/**
2119
	 * Object represented by this node
2120
	 *
2121
	 * @var Object
2122
	 */
2123
	protected $obj;
2124
2125
	/**
2126
	 * Edit link to the current record in the CMS
2127
	 *
2128
	 * @var string
2129
	 */
2130
	protected $link;
2131
2132
	/**
2133
	 * True if this is the currently selected node in the tree
2134
	 *
2135
	 * @var bool
2136
	 */
2137
	protected $isCurrent;
2138
2139
	/**
2140
	 * Name of method to count the number of children
2141
	 *
2142
	 * @var string
2143
	 */
2144
	protected $numChildrenMethod;
2145
2146
2147
	/**
2148
	 *
2149
	 * @var LeftAndMain_SearchFilter
2150
	 */
2151
	protected $filter;
2152
2153
	/**
2154
	 * @param Object $obj
2155
	 * @param string $link
2156
	 * @param bool $isCurrent
2157
	 * @param string $numChildrenMethod
2158
	 * @param LeftAndMain_SearchFilter $filter
2159
	 */
2160
	public function __construct($obj, $link = null, $isCurrent = false,
2161
		$numChildrenMethod = 'numChildren', $filter = null
2162
	) {
2163
		parent::__construct();
2164
		$this->obj = $obj;
2165
		$this->link = $link;
2166
		$this->isCurrent = $isCurrent;
2167
		$this->numChildrenMethod = $numChildrenMethod;
2168
		$this->filter = $filter;
2169
	}
2170
2171
	/**
2172
	 * Returns template, for further processing by {@link Hierarchy->getChildrenAsUL()}.
2173
	 * Does not include closing tag to allow this method to inject its own children.
2174
	 *
2175
	 * @todo Remove hardcoded assumptions around returning an <li>, by implementing recursive tree node rendering
2176
	 *
2177
	 * @return String
2178
	 */
2179
	public function forTemplate() {
2180
		$obj = $this->obj;
2181
		return "<li id=\"record-$obj->ID\" data-id=\"$obj->ID\" data-pagetype=\"$obj->ClassName\" class=\""
2182
			. $this->getClasses() . "\">" . "<ins class=\"jstree-icon\">&nbsp;</ins>"
2183
			. "<a href=\"" . $this->getLink() . "\" title=\"("
2184
			. trim(_t('LeftAndMain.PAGETYPE','Page type'), " :") // account for inconsistencies in translations
2185
			. ": " . $obj->i18n_singular_name() . ") $obj->Title\" ><ins class=\"jstree-icon\">&nbsp;</ins><span class=\"text\">" . ($obj->TreeTitle)
2186
			. "</span></a>";
2187
	}
2188
2189
	/**
2190
	 * Determine the CSS classes to apply to this node
2191
	 *
2192
	 * @return string
2193
	 */
2194
	public function getClasses() {
2195
		// Get classes from object
2196
		$classes = $this->obj->CMSTreeClasses($this->numChildrenMethod);
2197
		if($this->isCurrent) {
2198
			$classes .= ' current';
2199
		}
2200
		// Get status flag classes
2201
		$flags = $this->obj->hasMethod('getStatusFlags')
2202
			? $this->obj->getStatusFlags()
2203
			: false;
2204
		if ($flags) {
2205
			$statuses = array_keys($flags);
2206
			foreach ($statuses as $s) {
2207
				$classes .= ' status-' . $s;
2208
			}
2209
		}
2210
		// Get additional filter classes
2211
		if($this->filter && ($filterClasses = $this->filter->getPageClasses($this->obj))) {
2212
			if(is_array($filterClasses)) {
2213
				$filterClasses = implode(' ' . $filterClasses);
2214
			}
2215
			$classes .= ' ' . $filterClasses;
2216
		}
2217
		return $classes;
2218
	}
2219
2220
	public function getObj() {
2221
		return $this->obj;
2222
	}
2223
2224
	public function setObj($obj) {
2225
		$this->obj = $obj;
2226
		return $this;
2227
	}
2228
2229
	public function getLink() {
2230
		return $this->link;
2231
	}
2232
2233
	public function setLink($link) {
2234
		$this->link = $link;
2235
		return $this;
2236
	}
2237
2238
	public function getIsCurrent() {
2239
		return $this->isCurrent;
2240
	}
2241
2242
	public function setIsCurrent($bool) {
2243
		$this->isCurrent = $bool;
2244
		return $this;
2245
	}
2246
2247
}
2248
2249
/**
2250
 * Abstract interface for a class which may be used to filter the results displayed
2251
 * in a nested tree
2252
 */
2253
interface LeftAndMain_SearchFilter {
2254
2255
	/**
2256
	 * Method on {@link Hierarchy} objects which is used to traverse into children relationships.
2257
	 *
2258
	 * @return string
2259
	 */
2260
	public function getChildrenMethod();
2261
2262
	/**
2263
	 * Method on {@link Hierarchy} objects which is used find the number of children for a parent page
2264
	 *
2265
	 * @return string
2266
	 */
2267
	public function getNumChildrenMethod();
2268
2269
2270
	/**
2271
	 * Returns TRUE if the given page should be included in the tree.
2272
	 * Caution: Does NOT check view permissions on the page.
2273
	 *
2274
	 * @param DataObject $page
2275
	 * @return bool
2276
	 */
2277
	public function isPageIncluded($page);
2278
2279
	/**
2280
	 * Given a page, determine any additional CSS classes to apply to the tree node
2281
	 *
2282
	 * @param DataObject $page
2283
	 * @return array|string
2284
	 */
2285
	public function getPageClasses($page);
2286
}
2287