Completed
Push — master ( e01846...7c0007 )
by Damian
35s
created

LeftAndMain::save()   D

Complexity

Conditions 9
Paths 8

Size

Total Lines 40
Code Lines 28

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 1 Features 2
Metric Value
c 2
b 1
f 2
dl 0
loc 40
rs 4.909
cc 9
eloc 28
nc 8
nop 2
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.2';
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
	 * Returns the menu title for the given LeftAndMain subclass.
687
	 * Implemented static so that we can get this value without instantiating an object.
688
	 * Menu title is *not* internationalised.
689
	 */
690
	public static function menu_title_for_class($class) {
691
		$title = Config::inst()->get($class, 'menu_title', Config::FIRST_SET);
692
		if(!$title) $title = preg_replace('/Admin$/', '', $class);
693
		return $title;
694
	}
695
696
	/**
697
	 * Return styling for the menu icon, if a custom icon is set for this class
698
	 *
699
	 * Example: static $menu-icon = '/path/to/image/';
700
	 * @param string $class
701
	 * @return string
702
	 */
703
	public static function menu_icon_for_class($class) {
704
		$icon = Config::inst()->get($class, 'menu_icon', Config::FIRST_SET);
705
		if (!empty($icon)) {
706
			$class = strtolower(Convert::raw2htmlname(str_replace('\\', '-', $class)));
707
			return ".icon.icon-16.icon-{$class} { background-image: url('{$icon}'); } ";
708
		}
709
		return '';
710
	}
711
712
	public function show($request) {
713
		// TODO Necessary for TableListField URLs to work properly
714
		if($request->param('ID')) $this->setCurrentPageID($request->param('ID'));
715
		return $this->getResponseNegotiator()->respond($request);
716
	}
717
718
	/**
719
	 * Caution: Volatile API.
720
	 *
721
	 * @return PjaxResponseNegotiator
722
	 */
723
	public function getResponseNegotiator() {
724
		if(!$this->responseNegotiator) {
725
			$controller = $this;
726
			$this->responseNegotiator = new PjaxResponseNegotiator(
727
				array(
728
					'CurrentForm' => function() use(&$controller) {
729
						return $controller->getEditForm()->forTemplate();
730
					},
731
					'Content' => function() use(&$controller) {
732
						return $controller->renderWith($controller->getTemplatesWithSuffix('_Content'));
733
					},
734
					'Breadcrumbs' => function() use (&$controller) {
735
						return $controller->renderWith('CMSBreadcrumbs');
736
					},
737
					'default' => function() use(&$controller) {
738
						return $controller->renderWith($controller->getViewer('show'));
739
					}
740
				),
741
				$this->getResponse()
742
			);
743
		}
744
		return $this->responseNegotiator;
745
	}
746
747
	//------------------------------------------------------------------------------------------//
748
	// Main UI components
749
750
	/**
751
	 * Returns the main menu of the CMS.  This is also used by init()
752
	 * to work out which sections the user has access to.
753
	 *
754
	 * @param Boolean
755
	 * @return SS_List
756
	 */
757
	public function MainMenu($cached = true) {
758
		if(!isset($this->_cache_MainMenu) || !$cached) {
759
			// Don't accidentally return a menu if you're not logged in - it's used to determine access.
760
			if(!Member::currentUser()) return new ArrayList();
761
762
			// Encode into DO set
763
			$menu = new ArrayList();
764
			$menuItems = CMSMenu::get_viewable_menu_items();
765
766
			// extra styling for custom menu-icons
767
			$menuIconStyling = '';
768
769
			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...
770
				foreach($menuItems as $code => $menuItem) {
771
					// alternate permission checks (in addition to LeftAndMain->canView())
772
773
					if(
774
						isset($menuItem->controller)
775
						&& $this->hasMethod('alternateMenuDisplayCheck')
776
						&& !$this->alternateMenuDisplayCheck($menuItem->controller)
777
					) {
778
						continue;
779
					}
780
781
					$linkingmode = "link";
782
783
					if($menuItem->controller && get_class($this) == $menuItem->controller) {
784
						$linkingmode = "current";
785
					} else if(strpos($this->Link(), $menuItem->url) !== false) {
786
						if($this->Link() == $menuItem->url) {
787
							$linkingmode = "current";
788
789
						// default menu is the one with a blank {@link url_segment}
790
						} else if(singleton($menuItem->controller)->stat('url_segment') == '') {
791
							if($this->Link() == $this->stat('url_base').'/') {
792
								$linkingmode = "current";
793
							}
794
795
						} else {
796
							$linkingmode = "current";
797
						}
798
					}
799
800
					// already set in CMSMenu::populate_menu(), but from a static pre-controller
801
					// context, so doesn't respect the current user locale in _t() calls - as a workaround,
802
					// we simply call LeftAndMain::menu_title_for_class() again
803
					// if we're dealing with a controller
804
					if($menuItem->controller) {
805
						$defaultTitle = LeftAndMain::menu_title_for_class($menuItem->controller);
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

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

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

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

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

Loading history...
806
						$title = _t("{$menuItem->controller}.MENUTITLE", $defaultTitle);
807
					} else {
808
						$title = $menuItem->title;
809
					}
810
811
					// Provide styling for custom $menu-icon. Done here instead of in
812
					// CMSMenu::populate_menu(), because the icon is part of
813
					// the CMS right pane for the specified class as well...
814
					if($menuItem->controller) {
815
						$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...
816
						if (!empty($menuIcon)) $menuIconStyling .= $menuIcon;
817
					}
818
819
					$menu->push(new ArrayData(array(
820
						"MenuItem" => $menuItem,
821
						"AttributesHTML" => $menuItem->getAttributesHTML(),
822
						"Title" => Convert::raw2xml($title),
823
						"Code" => DBField::create_field('Text', $code),
824
						"Link" => $menuItem->url,
825
						"LinkingMode" => $linkingmode
826
					)));
827
				}
828
			}
829
			if ($menuIconStyling) Requirements::customCSS($menuIconStyling);
830
831
			$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...
832
		}
833
834
		return $this->_cache_MainMenu;
835
	}
836
837
	public function Menu() {
838
		return $this->renderWith($this->getTemplatesWithSuffix('_Menu'));
839
	}
840
841
	/**
842
	 * @todo Wrap in CMSMenu instance accessor
843
	 * @return ArrayData A single menu entry (see {@link MainMenu})
844
	 */
845
	public function MenuCurrentItem() {
846
		$items = $this->MainMenu();
847
		return $items->find('LinkingMode', 'current');
848
	}
849
850
	/**
851
	 * Return a list of appropriate templates for this class, with the given suffix using
852
	 * {@link SSViewer::get_templates_by_class()}
853
	 *
854
	 * @return array
855
	 */
856
	public function getTemplatesWithSuffix($suffix) {
857
		return SSViewer::get_templates_by_class(get_class($this), $suffix, 'LeftAndMain');
858
	}
859
860
	public function Content() {
861
		return $this->renderWith($this->getTemplatesWithSuffix('_Content'));
862
	}
863
864
	public function getRecord($id) {
865
		$className = $this->stat('tree_class');
866
		if($className && $id instanceof $className) {
867
			return $id;
868
		} else if($className && $id == 'root') {
869
			return singleton($className);
870
		} else if($className && is_numeric($id)) {
871
			return DataObject::get_by_id($className, $id);
872
		} else {
873
			return false;
874
		}
875
	}
876
877
	/**
878
	 * @return ArrayList
879
	 */
880
	public function Breadcrumbs($unlinked = false) {
881
		$defaultTitle = LeftAndMain::menu_title_for_class($this->class);
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

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

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

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

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

Loading history...
882
		$title = _t("{$this->class}.MENUTITLE", $defaultTitle);
883
		$items = new ArrayList(array(
884
			new ArrayData(array(
885
				'Title' => $title,
886
				'Link' => ($unlinked) ? false : $this->Link()
887
			))
888
		));
889
		$record = $this->currentPage();
890
		if($record && $record->exists()) {
891
			if($record->hasExtension('Hierarchy')) {
892
				$ancestors = $record->getAncestors();
893
				$ancestors = new ArrayList(array_reverse($ancestors->toArray()));
894
				$ancestors->push($record);
895
				foreach($ancestors as $ancestor) {
896
					$items->push(new ArrayData(array(
897
						'Title' => ($ancestor->MenuTitle) ? $ancestor->MenuTitle : $ancestor->Title,
898
						'Link' => ($unlinked) ? false : Controller::join_links($this->Link('show'), $ancestor->ID)
899
					)));
900
				}
901
			} else {
902
				$items->push(new ArrayData(array(
903
					'Title' => ($record->MenuTitle) ? $record->MenuTitle : $record->Title,
904
					'Link' => ($unlinked) ? false : Controller::join_links($this->Link('show'), $record->ID)
905
				)));
906
			}
907
		}
908
909
		return $items;
910
	}
911
912
	/**
913
	 * @return String HTML
914
	 */
915
	public function SiteTreeAsUL() {
916
		$html = $this->getSiteTreeFor($this->stat('tree_class'));
917
		$this->extend('updateSiteTreeAsUL', $html);
918
		return $html;
919
	}
920
921
	/**
922
	 * Gets the current search filter for this request, if available
923
	 *
924
	 * @throws InvalidArgumentException
925
	 * @return LeftAndMain_SearchFilter
926
	 */
927
	protected function getSearchFilter() {
928
		// Check for given FilterClass
929
		$params = $this->getRequest()->getVar('q');
930
		if(empty($params['FilterClass'])) {
931
			return null;
932
		}
933
934
		// Validate classname
935
		$filterClass = $params['FilterClass'];
936
		$filterInfo = new ReflectionClass($filterClass);
937
		if(!$filterInfo->implementsInterface('LeftAndMain_SearchFilter')) {
938
			throw new InvalidArgumentException(sprintf('Invalid filter class passed: %s', $filterClass));
939
		}
940
941
		return Injector::inst()->createWithArgs($filterClass, array($params));
942
	}
943
944
	/**
945
	 * Get a site tree HTML listing which displays the nodes under the given criteria.
946
	 *
947
	 * @param $className The class of the root object
948
	 * @param $rootID The ID of the root object.  If this is null then a complete tree will be
949
	 *  shown
950
	 * @param $childrenMethod The method to call to get the children of the tree. For example,
951
	 *  Children, AllChildrenIncludingDeleted, or AllHistoricalChildren
952
	 * @return String Nested unordered list with links to each page
953
	 */
954
	public function getSiteTreeFor($className, $rootID = null, $childrenMethod = null, $numChildrenMethod = null,
955
			$filterFunction = null, $nodeCountThreshold = 30) {
956
957
		// Filter criteria
958
		$filter = $this->getSearchFilter();
959
960
		// Default childrenMethod and numChildrenMethod
961
		if(!$childrenMethod) $childrenMethod = ($filter && $filter->getChildrenMethod())
962
			? $filter->getChildrenMethod()
963
			: 'AllChildrenIncludingDeleted';
964
965
		if(!$numChildrenMethod) {
966
			$numChildrenMethod = 'numChildren';
967
			if($filter && $filter->getNumChildrenMethod()) {
968
				$numChildrenMethod = $filter->getNumChildrenMethod();
969
			}
970
		}
971
		if(!$filterFunction && $filter) {
972
			$filterFunction = function($node) use($filter) {
973
				return $filter->isPageIncluded($node);
974
			};
975
		}
976
977
		// Get the tree root
978
		$record = ($rootID) ? $this->getRecord($rootID) : null;
979
		$obj = $record ? $record : singleton($className);
980
981
		// Get the current page
982
		// NOTE: This *must* be fetched before markPartialTree() is called, as this
983
		// causes the Hierarchy::$marked cache to be flushed (@see CMSMain::getRecord)
984
		// which means that deleted pages stored in the marked tree would be removed
985
		$currentPage = $this->currentPage();
986
987
		// Mark the nodes of the tree to return
988
		if ($filterFunction) $obj->setMarkingFilterFunction($filterFunction);
989
990
		$obj->markPartialTree($nodeCountThreshold, $this, $childrenMethod, $numChildrenMethod);
991
992
		// Ensure current page is exposed
993
		if($currentPage) $obj->markToExpose($currentPage);
994
995
		// NOTE: SiteTree/CMSMain coupling :-(
996
		if(class_exists('SiteTree')) {
997
			SiteTree::prepopulate_permission_cache('CanEditType', $obj->markedNodeIDs(),
998
				'SiteTree::can_edit_multiple');
999
		}
1000
1001
		// getChildrenAsUL is a flexible and complex way of traversing the tree
1002
		$controller = $this;
1003
		$recordController = ($this->stat('tree_class') == 'SiteTree') ?  singleton('CMSPageEditController') : $this;
1004
		$titleFn = function(&$child, $numChildrenMethod) use(&$controller, &$recordController, $filter) {
1005
			$link = Controller::join_links($recordController->Link("show"), $child->ID);
1006
			$node = LeftAndMain_TreeNode::create($child, $link, $controller->isCurrentPage($child), $numChildrenMethod, $filter);
1007
			return $node->forTemplate();
1008
		};
1009
1010
		// Limit the amount of nodes shown for performance reasons.
1011
		// Skip the check if we're filtering the tree, since its not clear how many children will
1012
		// match the filter criteria until they're queried (and matched up with previously marked nodes).
1013
		$nodeThresholdLeaf = Config::inst()->get('Hierarchy', 'node_threshold_leaf');
1014
		if($nodeThresholdLeaf && !$filterFunction) {
1015
			$nodeCountCallback = function($parent, $numChildren) use(&$controller, $className, $nodeThresholdLeaf) {
1016
				if($className == 'SiteTree' && $parent->ID && $numChildren > $nodeThresholdLeaf) {
1017
					return sprintf(
1018
						'<ul><li class="readonly"><span class="item">'
1019
							. '%s (<a href="%s" class="cms-panel-link" data-pjax-target="Content">%s</a>)'
1020
							. '</span></li></ul>',
1021
						_t('LeftAndMain.TooManyPages', 'Too many pages'),
1022
						Controller::join_links(
1023
							$controller->LinkWithSearch($controller->Link()), '
1024
							?view=list&ParentID=' . $parent->ID
1025
						),
1026
						_t(
1027
							'LeftAndMain.ShowAsList',
1028
							'show as list',
1029
							'Show large amount of pages in list instead of tree view'
1030
						)
1031
					);
1032
				}
1033
			};
1034
		} else {
1035
			$nodeCountCallback = null;
1036
		}
1037
1038
		// If the amount of pages exceeds the node thresholds set, use the callback
1039
		$html = null;
1040
		if($obj->ParentID && $nodeCountCallback) {
1041
			$html = $nodeCountCallback($obj, $obj->$numChildrenMethod());
1042
		}
1043
1044
		// Otherwise return the actual tree (which might still filter leaf thresholds on children)
1045
		if(!$html) {
1046
			$html = $obj->getChildrenAsUL(
1047
				"",
1048
				$titleFn,
1049
				singleton('CMSPagesController'),
1050
				true,
1051
				$childrenMethod,
1052
				$numChildrenMethod,
1053
				$nodeCountThreshold,
1054
				$nodeCountCallback
1055
			);
1056
		}
1057
1058
		// Wrap the root if needs be.
1059
		if(!$rootID) {
1060
			$rootLink = $this->Link('show') . '/root';
1061
1062
			// This lets us override the tree title with an extension
1063
			if($this->hasMethod('getCMSTreeTitle') && $customTreeTitle = $this->getCMSTreeTitle()) {
1064
				$treeTitle = $customTreeTitle;
1065
			} elseif(class_exists('SiteConfig')) {
1066
				$siteConfig = SiteConfig::current_site_config();
1067
				$treeTitle =  Convert::raw2xml($siteConfig->Title);
1068
			} else {
1069
				$treeTitle = '...';
1070
			}
1071
1072
			$html = "<ul><li id=\"record-0\" data-id=\"0\" class=\"Root nodelete\"><strong>$treeTitle</strong>"
1073
				. $html . "</li></ul>";
1074
		}
1075
1076
		return $html;
1077
	}
1078
1079
	/**
1080
	 * Get a subtree underneath the request param 'ID'.
1081
	 * If ID = 0, then get the whole tree.
1082
	 */
1083
	public function getsubtree($request) {
1084
		$html = $this->getSiteTreeFor(
1085
			$this->stat('tree_class'),
1086
			$request->getVar('ID'),
1087
			null,
1088
			null,
1089
			null,
1090
			$request->getVar('minNodeCount')
1091
		);
1092
1093
		// Trim off the outer tag
1094
		$html = preg_replace('/^[\s\t\r\n]*<ul[^>]*>/','', $html);
1095
		$html = preg_replace('/<\/ul[^>]*>[\s\t\r\n]*$/','', $html);
1096
1097
		return $html;
1098
	}
1099
1100
	/**
1101
	 * Allows requesting a view update on specific tree nodes.
1102
	 * Similar to {@link getsubtree()}, but doesn't enforce loading
1103
	 * all children with the node. Useful to refresh views after
1104
	 * state modifications, e.g. saving a form.
1105
	 *
1106
	 * @return String JSON
1107
	 */
1108
	public function updatetreenodes($request) {
1109
		$data = array();
1110
		$ids = explode(',', $request->getVar('ids'));
1111
		foreach($ids as $id) {
1112
			if($id === "") continue; // $id may be a blank string, which is invalid and should be skipped over
1113
1114
			$record = $this->getRecord($id);
1115
			if(!$record) continue; // In case a page is no longer available
1116
			$recordController = ($this->stat('tree_class') == 'SiteTree')
1117
				?  singleton('CMSPageEditController')
1118
				: $this;
1119
1120
			// Find the next & previous nodes, for proper positioning (Sort isn't good enough - it's not a raw offset)
1121
			// TODO: These methods should really be in hierarchy - for a start it assumes Sort exists
1122
			$next = $prev = null;
1123
1124
			$className = $this->stat('tree_class');
1125
			$next = DataObject::get($className)
1126
				->filter('ParentID', $record->ParentID)
1127
				->filter('Sort:GreaterThan', $record->Sort)
1128
				->first();
1129
1130
			if (!$next) {
1131
				$prev = DataObject::get($className)
1132
					->filter('ParentID', $record->ParentID)
1133
					->filter('Sort:LessThan', $record->Sort)
1134
					->reverse()
1135
					->first();
1136
			}
1137
1138
			$link = Controller::join_links($recordController->Link("show"), $record->ID);
1139
			$html = LeftAndMain_TreeNode::create($record, $link, $this->isCurrentPage($record))
1140
				->forTemplate() . '</li>';
1141
1142
			$data[$id] = array(
1143
				'html' => $html,
1144
				'ParentID' => $record->ParentID,
1145
				'NextID' => $next ? $next->ID : null,
1146
				'PrevID' => $prev ? $prev->ID : null
1147
			);
1148
		}
1149
		$this->getResponse()->addHeader('Content-Type', 'text/json');
1150
		return Convert::raw2json($data);
1151
	}
1152
1153
	/**
1154
	 * Save  handler
1155
	 *
1156
	 * @param array $data
1157
	 * @param Form $form
1158
	 * @return SS_HTTPResponse
1159
	 */
1160
	public function save($data, $form) {
1161
		$request = $this->getRequest();
1162
		$className = $this->stat('tree_class');
1163
1164
		// Existing or new record?
1165
		$id = $data['ID'];
1166
		if(is_numeric($id) && $id > 0) {
1167
			$record = DataObject::get_by_id($className, $id);
1168
			if($record && !$record->canEdit()) {
1169
				return Security::permissionFailure($this);
1170
			}
1171
			if(!$record || !$record->ID) {
1172
				$this->httpError(404, "Bad record ID #" . (int)$id);
1173
			}
1174
		} else {
1175
			if(!singleton($this->stat('tree_class'))->canCreate()) {
1176
				return Security::permissionFailure($this);
1177
			}
1178
			$record = $this->getNewItem($id, false);
1179
		}
1180
1181
		// save form data into record
1182
		$form->saveInto($record, true);
1183
		$record->write();
1184
		$this->extend('onAfterSave', $record);
1185
		$this->setCurrentPageID($record->ID);
1186
1187
		$message = _t('LeftAndMain.SAVEDUP', 'Saved.');
1188
		if($request->getHeader('X-Formschema-Request')) {
1189
			$form->setMessage($message, 'good');
1190
			$data = $this->getSchemaForForm($form);
1191
			$response = new SS_HTTPResponse(Convert::raw2json($data));
1192
			$response->addHeader('Content-Type', 'application/json');
1193
		} else {
1194
			$response = $this->getResponseNegotiator()->respond($request);
1195
		}
1196
1197
		$response->addHeader('X-Status', rawurlencode($message));
1198
		return $response;
1199
	}
1200
1201
	/**
1202
	 * Create new item.
1203
	 *
1204
	 * @param string|int $id
1205
	 * @param bool $setID
1206
	 * @return DataObject
1207
	 */
1208
	public function getNewItem($id, $setID = true) {
1209
		$class = $this->stat('tree_class');
1210
		$object = Injector::inst()->create($class);
1211
		if($setID) {
1212
			$object->ID = $id;
1213
		}
1214
		return $object;
1215
	}
1216
1217
	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...
1218
		$className = $this->stat('tree_class');
1219
1220
		$id = $data['ID'];
1221
		$record = DataObject::get_by_id($className, $id);
1222
		if($record && !$record->canDelete()) return Security::permissionFailure();
1223
		if(!$record || !$record->ID) $this->httpError(404, "Bad record ID #" . (int)$id);
1224
1225
		$record->delete();
1226
1227
		$this->getResponse()->addHeader('X-Status', rawurlencode(_t('LeftAndMain.DELETED', 'Deleted.')));
1228
		return $this->getResponseNegotiator()->respond(
1229
			$this->getRequest(),
1230
			array('currentform' => array($this, 'EmptyForm'))
1231
		);
1232
	}
1233
1234
	/**
1235
	 * Update the position and parent of a tree node.
1236
	 * Only saves the node if changes were made.
1237
	 *
1238
	 * Required data:
1239
	 * - 'ID': The moved node
1240
	 * - 'ParentID': New parent relation of the moved node (0 for root)
1241
	 * - 'SiblingIDs': Array of all sibling nodes to the moved node (incl. the node itself).
1242
	 *   In case of a 'ParentID' change, relates to the new siblings under the new parent.
1243
	 *
1244
	 * @return SS_HTTPResponse JSON string with a
1245
	 */
1246
	public function savetreenode($request) {
1247
		if (!Permission::check('SITETREE_REORGANISE') && !Permission::check('ADMIN')) {
1248
			$this->getResponse()->setStatusCode(
1249
				403,
1250
				_t('LeftAndMain.CANT_REORGANISE',
1251
					"You do not have permission to rearange the site tree. Your change was not saved.")
1252
			);
1253
			return;
1254
		}
1255
1256
		$className = $this->stat('tree_class');
1257
		$statusUpdates = array('modified'=>array());
1258
		$id = $request->requestVar('ID');
1259
		$parentID = $request->requestVar('ParentID');
1260
1261
		if($className == 'SiteTree' && $page = DataObject::get_by_id('Page', $id)){
1262
			$root = $page->getParentType();
1263
			if(($parentID == '0' || $root == 'root') && !SiteConfig::current_site_config()->canCreateTopLevel()){
1264
				$this->getResponse()->setStatusCode(
1265
					403,
1266
					_t('LeftAndMain.CANT_REORGANISE',
1267
						"You do not have permission to alter Top level pages. Your change was not saved.")
1268
					);
1269
				return;
1270
			}
1271
		}
1272
1273
		$siblingIDs = $request->requestVar('SiblingIDs');
1274
		$statusUpdates = array('modified'=>array());
1275
		if(!is_numeric($id) || !is_numeric($parentID)) throw new InvalidArgumentException();
1276
1277
		$node = DataObject::get_by_id($className, $id);
1278
		if($node && !$node->canEdit()) return Security::permissionFailure($this);
1279
1280
		if(!$node) {
1281
			$this->getResponse()->setStatusCode(
1282
				500,
1283
				_t('LeftAndMain.PLEASESAVE',
1284
					"Please Save Page: This page could not be updated because it hasn't been saved yet."
1285
				)
1286
			);
1287
			return;
1288
		}
1289
1290
		// Update hierarchy (only if ParentID changed)
1291
		if($node->ParentID != $parentID) {
1292
			$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...
1293
			$node->write();
1294
1295
			$statusUpdates['modified'][$node->ID] = array(
1296
				'TreeTitle'=>$node->TreeTitle
1297
			);
1298
1299
			// Update all dependent pages
1300
			if(class_exists('VirtualPage')) {
1301
				$virtualPages = VirtualPage::get()->filter("CopyContentFromID", $node->ID);
1302
				foreach($virtualPages as $virtualPage) {
1303
					$statusUpdates['modified'][$virtualPage->ID] = array(
1304
						'TreeTitle' => $virtualPage->TreeTitle()
1305
					);
1306
				}
1307
			}
1308
1309
			$this->getResponse()->addHeader('X-Status',
1310
				rawurlencode(_t('LeftAndMain.REORGANISATIONSUCCESSFUL', 'Reorganised the site tree successfully.')));
1311
		}
1312
1313
		// Update sorting
1314
		if(is_array($siblingIDs)) {
1315
			$counter = 0;
1316
			foreach($siblingIDs as $id) {
1317
				if($id == $node->ID) {
1318
					$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...
1319
					$node->write();
1320
					$statusUpdates['modified'][$node->ID] = array(
1321
						'TreeTitle' => $node->TreeTitle
1322
					);
1323
				} else if(is_numeric($id)) {
1324
					// Nodes that weren't "actually moved" shouldn't be registered as
1325
					// having been edited; do a direct SQL update instead
1326
					++$counter;
1327
					DB::prepared_query(
1328
						"UPDATE \"$className\" SET \"Sort\" = ? WHERE \"ID\" = ?",
1329
						array($counter, $id)
1330
					);
1331
				}
1332
			}
1333
1334
			$this->getResponse()->addHeader('X-Status',
1335
				rawurlencode(_t('LeftAndMain.REORGANISATIONSUCCESSFUL', 'Reorganised the site tree successfully.')));
1336
		}
1337
1338
		return Convert::raw2json($statusUpdates);
1339
	}
1340
1341
	public function CanOrganiseSitetree() {
1342
		return !Permission::check('SITETREE_REORGANISE') && !Permission::check('ADMIN') ? false : true;
1343
	}
1344
1345
	/**
1346
	 * Retrieves an edit form, either for display, or to process submitted data.
1347
	 * Also used in the template rendered through {@link Right()} in the $EditForm placeholder.
1348
	 *
1349
	 * This is a "pseudo-abstract" methoed, usually connected to a {@link getEditForm()}
1350
	 * method in an entwine subclass. This method can accept a record identifier,
1351
	 * selected either in custom logic, or through {@link currentPageID()}.
1352
	 * The form usually construct itself from {@link DataObject->getCMSFields()}
1353
	 * for the specific managed subclass defined in {@link LeftAndMain::$tree_class}.
1354
	 *
1355
	 * @param HTTPRequest $request Optionally contains an identifier for the
1356
	 *  record to load into the form.
1357
	 * @return Form Should return a form regardless wether a record has been found.
1358
	 *  Form might be readonly if the current user doesn't have the permission to edit
1359
	 *  the record.
1360
	 */
1361
	/**
1362
	 * @return Form
1363
	 */
1364
	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...
1365
		return $this->getEditForm();
1366
	}
1367
1368
	/**
1369
	 * Calls {@link SiteTree->getCMSFields()}
1370
	 *
1371
	 * @param Int $id
1372
	 * @param FieldList $fields
1373
	 * @return Form
1374
	 */
1375
	public function getEditForm($id = null, $fields = null) {
1376
		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...
1377
1378
		if(is_object($id)) {
1379
			$record = $id;
1380
		} else {
1381
			$record = $this->getRecord($id);
1382
			if($record && !$record->canView()) return Security::permissionFailure($this);
1383
		}
1384
1385
		if($record) {
1386
			$fields = ($fields) ? $fields : $record->getCMSFields();
1387
			if ($fields == null) {
1388
				user_error(
1389
					"getCMSFields() returned null  - it should return a FieldList object.
1390
					Perhaps you forgot to put a return statement at the end of your method?",
1391
					E_USER_ERROR
1392
				);
1393
			}
1394
1395
			// Add hidden fields which are required for saving the record
1396
			// and loading the UI state
1397
			if(!$fields->dataFieldByName('ClassName')) {
1398
				$fields->push(new HiddenField('ClassName'));
1399
			}
1400
1401
			$tree_class = $this->stat('tree_class');
1402
			if(
1403
				$tree_class::has_extension('Hierarchy')
1404
				&& !$fields->dataFieldByName('ParentID')
1405
			) {
1406
				$fields->push(new HiddenField('ParentID'));
1407
			}
1408
1409
			// Added in-line to the form, but plucked into different view by frontend scripts.
1410
			if(in_array('CMSPreviewable', class_implements($record))) {
1411
				$navField = new LiteralField('SilverStripeNavigator', $this->getSilverStripeNavigator());
1412
				$navField->setAllowHTML(true);
1413
				$fields->push($navField);
1414
			}
1415
1416
			if($record->hasMethod('getAllCMSActions')) {
1417
				$actions = $record->getAllCMSActions();
1418
			} else {
1419
				$actions = $record->getCMSActions();
1420
				// add default actions if none are defined
1421
				if(!$actions || !$actions->Count()) {
1422
					if($record->hasMethod('canEdit') && $record->canEdit()) {
1423
						$actions->push(
1424
							FormAction::create('save',_t('CMSMain.SAVE','Save'))
1425
								->addExtraClass('ss-ui-action-constructive')->setAttribute('data-icon', 'accept')
1426
						);
1427
					}
1428
					if($record->hasMethod('canDelete') && $record->canDelete()) {
1429
						$actions->push(
1430
							FormAction::create('delete',_t('ModelAdmin.DELETE','Delete'))
1431
								->addExtraClass('ss-ui-action-destructive')
1432
						);
1433
					}
1434
				}
1435
			}
1436
1437
			// Use <button> to allow full jQuery UI styling
1438
			$actionsFlattened = $actions->dataFields();
1439
			if($actionsFlattened) foreach($actionsFlattened as $action) $action->setUseButtonTag(true);
1440
1441
			$negotiator = $this->getResponseNegotiator();
1442
			$form = Form::create(
1443
				$this, "EditForm", $fields, $actions
1444
			)->setHTMLID('Form_EditForm');
1445
			$form->addExtraClass('cms-edit-form');
1446
			$form->loadDataFrom($record);
1447
			$form->setTemplate($this->getTemplatesWithSuffix('_EditForm'));
1448
			$form->setAttribute('data-pjax-fragment', 'CurrentForm');
1449
			$form->setValidationResponseCallback(function() use ($negotiator, $form) {
1450
				$request = $this->getRequest();
1451
				if($request->isAjax() && $negotiator) {
1452
					$form->setupFormErrors();
1453
					$result = $form->forTemplate();
1454
1455
					return $negotiator->respond($request, array(
1456
						'CurrentForm' => function() use($result) {
1457
							return $result;
1458
						}
1459
					));
1460
				}
1461
			});
1462
1463
			// Announce the capability so the frontend can decide whether to allow preview or not.
1464
			if(in_array('CMSPreviewable', class_implements($record))) {
1465
				$form->addExtraClass('cms-previewable');
1466
			}
1467
1468
			// Set this if you want to split up tabs into a separate header row
1469
			// 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...
1470
			// 	$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...
1471
			// }
1472
1473
			// Add a default or custom validator.
1474
			// @todo Currently the default Validator.js implementation
1475
			//  adds javascript to the document body, meaning it won't
1476
			//  be included properly if the associated fields are loaded
1477
			//  through ajax. This means only serverside validation
1478
			//  will kick in for pages+validation loaded through ajax.
1479
			//  This will be solved by using less obtrusive javascript validation
1480
			//  in the future, see http://open.silverstripe.com/ticket/2915 and
1481
			//  http://open.silverstripe.com/ticket/3386
1482
			if($record->hasMethod('getCMSValidator')) {
1483
				$validator = $record->getCMSValidator();
1484
				// The clientside (mainly LeftAndMain*.js) rely on ajax responses
1485
				// which can be evaluated as javascript, hence we need
1486
				// to override any global changes to the validation handler.
1487
				if($validator != NULL){
1488
					$form->setValidator($validator);
1489
				}
1490
			} else {
1491
				$form->unsetValidator();
1492
			}
1493
1494
			if($record->hasMethod('canEdit') && !$record->canEdit()) {
1495
				$readonlyFields = $form->Fields()->makeReadonly();
1496
				$form->setFields($readonlyFields);
1497
			}
1498
		} else {
1499
			$form = $this->EmptyForm();
1500
		}
1501
1502
		return $form;
1503
	}
1504
1505
	/**
1506
	 * Returns a placeholder form, used by {@link getEditForm()} if no record is selected.
1507
	 * Our javascript logic always requires a form to be present in the CMS interface.
1508
	 *
1509
	 * @return Form
1510
	 */
1511
	public function EmptyForm() {
1512
		$form = Form::create(
1513
			$this,
1514
			"EditForm",
1515
			new FieldList(
1516
				// new HeaderField(
1517
				// 	'WelcomeHeader',
1518
				// 	$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...
1519
				// ),
1520
				// new LiteralField(
1521
				// 	'WelcomeText',
1522
				// 	sprintf('<p id="WelcomeMessage">%s %s. %s</p>',
1523
				// 		_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...
1524
				// 		$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...
1525
				// 		_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...
1526
				// 	)
1527
				// )
1528
			),
1529
			new FieldList()
1530
		)->setHTMLID('Form_EditForm');
1531
		$form->unsetValidator();
1532
		$form->addExtraClass('cms-edit-form');
1533
		$form->addExtraClass('root-form');
1534
		$form->setTemplate($this->getTemplatesWithSuffix('_EditForm'));
1535
		$form->setAttribute('data-pjax-fragment', 'CurrentForm');
1536
1537
		return $form;
1538
	}
1539
1540
	/**
1541
	 * Return the CMS's HTML-editor toolbar
1542
	 */
1543
	public function EditorToolbar() {
1544
		return HtmlEditorField_Toolbar::create($this, "EditorToolbar");
1545
	}
1546
1547
	/**
1548
	 * Renders a panel containing tools which apply to all displayed
1549
	 * "content" (mostly through {@link EditForm()}), for example a tree navigation or a filter panel.
1550
	 * Auto-detects applicable templates by naming convention: "<controller classname>_Tools.ss",
1551
	 * and takes the most specific template (see {@link getTemplatesWithSuffix()}).
1552
	 * To explicitly disable the panel in the subclass, simply create a more specific, empty template.
1553
	 *
1554
	 * @return String HTML
1555
	 */
1556
	public function Tools() {
1557
		$templates = $this->getTemplatesWithSuffix('_Tools');
1558
		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...
1559
			$viewer = new SSViewer($templates);
1560
			return $viewer->process($this);
1561
		} else {
1562
			return false;
1563
		}
1564
	}
1565
1566
	/**
1567
	 * Renders a panel containing tools which apply to the currently displayed edit form.
1568
	 * The main difference to {@link Tools()} is that the panel is displayed within
1569
	 * the element structure of the form panel (rendered through {@link EditForm}).
1570
	 * This means the panel will be loaded alongside new forms, and refreshed upon save,
1571
	 * which can mean a performance hit, depending on how complex your panel logic gets.
1572
	 * Any form fields contained in the returned markup will also be submitted with the main form,
1573
	 * which might be desired depending on the implementation details.
1574
	 *
1575
	 * @return String HTML
1576
	 */
1577
	public function EditFormTools() {
1578
		$templates = $this->getTemplatesWithSuffix('_EditFormTools');
1579
		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...
1580
			$viewer = new SSViewer($templates);
1581
			return $viewer->process($this);
1582
		} else {
1583
			return false;
1584
		}
1585
	}
1586
1587
	/**
1588
	 * Batch Actions Handler
1589
	 */
1590
	public function batchactions() {
1591
		return new CMSBatchActionHandler($this, 'batchactions', $this->stat('tree_class'));
1592
	}
1593
1594
	/**
1595
	 * @return Form
1596
	 */
1597
	public function BatchActionsForm() {
1598
		$actions = $this->batchactions()->batchActionList();
1599
		$actionsMap = array('-1' => _t('LeftAndMain.DropdownBatchActionsDefault', 'Choose an action...')); // Placeholder action
1600
		foreach($actions as $action) {
1601
			$actionsMap[$action->Link] = $action->Title;
1602
		}
1603
1604
		$form = new Form(
1605
			$this,
1606
			'BatchActionsForm',
1607
			new FieldList(
1608
				new HiddenField('csvIDs'),
1609
				DropdownField::create(
1610
					'Action',
1611
					false,
1612
					$actionsMap
1613
				)
1614
					->setAttribute('autocomplete', 'off')
1615
					->setAttribute('data-placeholder', _t('LeftAndMain.DropdownBatchActionsDefault', 'Choose an action...'))
1616
			),
1617
			new FieldList(
1618
				// TODO i18n
1619
				new FormAction('submit', _t('Form.SubmitBtnLabel', "Go"))
1620
			)
1621
		);
1622
		$form->addExtraClass('cms-batch-actions nostyle');
1623
		$form->unsetValidator();
1624
1625
		$this->extend('updateBatchActionsForm', $form);
1626
		return $form;
1627
	}
1628
1629
	public function printable() {
1630
		$form = $this->getEditForm($this->currentPageID());
1631
		if(!$form) return false;
1632
1633
		$form->transform(new PrintableTransformation());
1634
		$form->setActions(null);
1635
1636
		Requirements::clear();
1637
		Requirements::css(FRAMEWORK_ADMIN_DIR . '/dist/css/LeftAndMain_printable.css');
1638
		return array(
1639
			"PrintForm" => $form
1640
		);
1641
	}
1642
1643
	/**
1644
	 * Used for preview controls, mainly links which switch between different states of the page.
1645
	 *
1646
	 * @return ArrayData
1647
	 */
1648
	public function getSilverStripeNavigator() {
1649
		$page = $this->currentPage();
1650
		if($page) {
1651
			$navigator = new SilverStripeNavigator($page);
1652
			return $navigator->renderWith($this->getTemplatesWithSuffix('_SilverStripeNavigator'));
1653
		} else {
1654
			return false;
1655
		}
1656
	}
1657
1658
	/**
1659
	 * Identifier for the currently shown record,
1660
	 * in most cases a database ID. Inspects the following
1661
	 * sources (in this order):
1662
	 * - GET/POST parameter named 'ID'
1663
	 * - URL parameter named 'ID'
1664
	 * - Session value namespaced by classname, e.g. "CMSMain.currentPage"
1665
	 *
1666
	 * @return int
1667
	 */
1668
	public function currentPageID() {
1669
		if($this->getRequest()->requestVar('ID') && is_numeric($this->getRequest()->requestVar('ID')))	{
1670
			return $this->getRequest()->requestVar('ID');
1671
		} elseif (isset($this->urlParams['ID']) && is_numeric($this->urlParams['ID'])) {
1672
			return $this->urlParams['ID'];
1673
		} elseif(Session::get($this->sessionNamespace() . ".currentPage")) {
1674
			return Session::get($this->sessionNamespace() . ".currentPage");
1675
		} else {
1676
			return null;
1677
		}
1678
	}
1679
1680
	/**
1681
	 * Forces the current page to be set in session,
1682
	 * which can be retrieved later through {@link currentPageID()}.
1683
	 * Keep in mind that setting an ID through GET/POST or
1684
	 * as a URL parameter will overrule this value.
1685
	 *
1686
	 * @param int $id
1687
	 */
1688
	public function setCurrentPageID($id) {
1689
		Session::set($this->sessionNamespace() . ".currentPage", $id);
1690
	}
1691
1692
	/**
1693
	 * Uses {@link getRecord()} and {@link currentPageID()}
1694
	 * to get the currently selected record.
1695
	 *
1696
	 * @return DataObject
1697
	 */
1698
	public function currentPage() {
1699
		return $this->getRecord($this->currentPageID());
1700
	}
1701
1702
	/**
1703
	 * Compares a given record to the currently selected one (if any).
1704
	 * Used for marking the current tree node.
1705
	 *
1706
	 * @return boolean
1707
	 */
1708
	public function isCurrentPage(DataObject $record) {
1709
		return ($record->ID == $this->currentPageID());
1710
	}
1711
1712
	/**
1713
	 * @return String
1714
	 */
1715
	protected function sessionNamespace() {
1716
		$override = $this->stat('session_namespace');
1717
		return $override ? $override : $this->class;
1718
	}
1719
1720
	/**
1721
	 * URL to a previewable record which is shown through this controller.
1722
	 * The controller might not have any previewable content, in which case
1723
	 * this method returns FALSE.
1724
	 *
1725
	 * @return String|boolean
1726
	 */
1727
	public function LinkPreview() {
1728
		return false;
1729
	}
1730
1731
	/**
1732
	 * Return the version number of this application.
1733
	 * Uses the number in <mymodule>/silverstripe_version
1734
	 * (automatically replaced by build scripts).
1735
	 * If silverstripe_version is empty,
1736
	 * then attempts to get it from composer.lock
1737
	 *
1738
	 * @return string
1739
	 */
1740
	public function CMSVersion() {
1741
		$versions = array();
1742
		$modules = array(
1743
			'silverstripe/framework' => array(
1744
				'title' => 'Framework',
1745
				'versionFile' => FRAMEWORK_PATH . '/silverstripe_version',
1746
			)
1747
		);
1748
		if(defined('CMS_PATH')) {
1749
			$modules['silverstripe/cms'] = array(
1750
				'title' => 'CMS',
1751
				'versionFile' => CMS_PATH . '/silverstripe_version',
1752
			);
1753
		}
1754
1755
		// Tries to obtain version number from composer.lock if it exists
1756
		$composerLockPath = BASE_PATH . '/composer.lock';
1757
		if (file_exists($composerLockPath)) {
1758
			$cache = SS_Cache::factory('LeftAndMain_CMSVersion');
1759
			$cacheKey = filemtime($composerLockPath);
1760
			$versions = $cache->load($cacheKey);
1761
			if($versions) {
1762
				$versions = json_decode($versions, true);
1763
			} else {
1764
				$versions = array();
1765
			}
1766
			if(!$versions && $jsonData = file_get_contents($composerLockPath)) {
1767
				$lockData = json_decode($jsonData);
1768
				if($lockData && isset($lockData->packages)) {
1769
					foreach ($lockData->packages as $package) {
1770
						if(
1771
							array_key_exists($package->name, $modules)
1772
							&& isset($package->version)
1773
						) {
1774
							$versions[$package->name] = $package->version;
1775
						}
1776
					}
1777
					$cache->save(json_encode($versions), $cacheKey);
1778
				}
1779
			}
1780
		}
1781
1782
		// Fall back to static version file
1783
		foreach($modules as $moduleName => $moduleSpec) {
1784
			if(!isset($versions[$moduleName])) {
1785
				if($staticVersion = file_get_contents($moduleSpec['versionFile'])) {
1786
					$versions[$moduleName] = $staticVersion;
1787
				} else {
1788
					$versions[$moduleName] = _t('LeftAndMain.VersionUnknown', 'Unknown');
1789
				}
1790
			}
1791
		}
1792
1793
		$out = array();
1794
		foreach($modules as $moduleName => $moduleSpec) {
1795
			$out[] = $modules[$moduleName]['title'] . ': ' . $versions[$moduleName];
1796
		}
1797
		return implode(', ', $out);
1798
	}
1799
1800
	/**
1801
	 * @return array
1802
	 */
1803
	public function SwitchView() {
1804
		if($page = $this->currentPage()) {
1805
			$nav = SilverStripeNavigator::get_for_record($page);
1806
			return $nav['items'];
1807
		}
1808
	}
1809
1810
	/**
1811
	 * @return SiteConfig
1812
	 */
1813
	public function SiteConfig() {
1814
		return (class_exists('SiteConfig')) ? SiteConfig::current_site_config() : null;
1815
	}
1816
1817
	/**
1818
	 * The href for the anchor on the Silverstripe logo.
1819
	 * Set by calling LeftAndMain::set_application_link()
1820
	 *
1821
	 * @config
1822
	 * @var String
1823
	 */
1824
	private static $application_link = '//www.silverstripe.org/';
1825
1826
	/**
1827
	 * Sets the href for the anchor on the Silverstripe logo in the menu
1828
	 *
1829
	 * @deprecated since version 4.0
1830
	 *
1831
	 * @param String $link
1832
	 */
1833
	public static function set_application_link($link) {
1834
		Deprecation::notice('4.0', 'Use the "LeftAndMain.application_link" config setting instead');
1835
		Config::inst()->update('LeftAndMain', 'application_link', $link);
1836
	}
1837
1838
	/**
1839
	 * @return String
1840
	 */
1841
	public function ApplicationLink() {
1842
		return $this->stat('application_link');
1843
	}
1844
1845
	/**
1846
	 * The application name. Customisable by calling
1847
	 * LeftAndMain::setApplicationName() - the first parameter.
1848
	 *
1849
	 * @config
1850
	 * @var String
1851
	 */
1852
	private static $application_name = 'SilverStripe';
1853
1854
	/**
1855
	 * @param String $name
1856
	 * @deprecated since version 4.0
1857
	 */
1858
	public static function setApplicationName($name) {
1859
		Deprecation::notice('4.0', 'Use the "LeftAndMain.application_name" config setting instead');
1860
		Config::inst()->update('LeftAndMain', 'application_name', $name);
1861
	}
1862
1863
	/**
1864
	 * Get the application name.
1865
	 *
1866
	 * @return string
1867
	 */
1868
	public function getApplicationName() {
1869
		return $this->stat('application_name');
1870
	}
1871
1872
	/**
1873
	 * @return string
1874
	 */
1875
	public function Title() {
1876
		$app = $this->getApplicationName();
1877
1878
		return ($section = $this->SectionTitle()) ? sprintf('%s - %s', $app, $section) : $app;
1879
	}
1880
1881
	/**
1882
	 * Return the title of the current section. Either this is pulled from
1883
	 * the current panel's menu_title or from the first active menu
1884
	 *
1885
	 * @return string
1886
	 */
1887
	public function SectionTitle() {
1888
		$class = get_class($this);
1889
		$defaultTitle = LeftAndMain::menu_title_for_class($class);
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

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

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

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

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

Loading history...
1890
		if($title = _t("{$class}.MENUTITLE", $defaultTitle)) return $title;
1891
1892
		foreach($this->MainMenu() as $menuItem) {
1893
			if($menuItem->LinkingMode != 'link') return $menuItem->Title;
1894
		}
1895
	}
1896
1897
	/**
1898
	 * Same as {@link ViewableData->CSSClasses()}, but with a changed name
1899
	 * to avoid problems when using {@link ViewableData->customise()}
1900
	 * (which always returns "ArrayData" from the $original object).
1901
	 *
1902
	 * @return String
1903
	 */
1904
	public function BaseCSSClasses() {
1905
		return $this->CSSClasses('Controller');
1906
	}
1907
1908
	/**
1909
	 * @return String
1910
	 */
1911
	public function Locale() {
1912
		return DBField::create_field('Locale', i18n::get_locale());
1913
	}
1914
1915
	public function providePermissions() {
1916
		$perms = array(
1917
			"CMS_ACCESS_LeftAndMain" => array(
1918
				'name' => _t('CMSMain.ACCESSALLINTERFACES', 'Access to all CMS sections'),
1919
				'category' => _t('Permission.CMS_ACCESS_CATEGORY', 'CMS Access'),
1920
				'help' => _t('CMSMain.ACCESSALLINTERFACESHELP', 'Overrules more specific access settings.'),
1921
				'sort' => -100
1922
			)
1923
		);
1924
1925
		// Add any custom ModelAdmin subclasses. Can't put this on ModelAdmin itself
1926
		// since its marked abstract, and needs to be singleton instanciated.
1927
		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...
1928
			if($class == 'ModelAdmin') continue;
1929
			if(ClassInfo::classImplements($class, 'TestOnly')) continue;
1930
1931
			$title = _t("{$class}.MENUTITLE", LeftAndMain::menu_title_for_class($class));
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

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

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

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

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

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