Completed
Push — master ( f39c4d...b2e354 )
by Sam
03:35 queued 03:17
created

LeftAndMain::SiteTreeAsUL()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 4
nc 1
nop 0
dl 0
loc 5
rs 9.4285
c 0
b 0
f 0
1
<?php
2
3
namespace SilverStripe\Admin;
4
5
use SilverStripe\CMS\Controllers\CMSPageEditController;
6
use SilverStripe\CMS\Controllers\CMSPagesController;
7
use SilverStripe\CMS\Model\SiteTree;
8
use SilverStripe\CMS\Model\VirtualPage;
9
use SilverStripe\CMS\Controllers\SilverStripeNavigator;
10
use SilverStripe\Control\ContentNegotiator;
11
use SilverStripe\Control\Director;
12
use SilverStripe\Control\HTTPResponse;
13
use SilverStripe\Control\Session;
14
use SilverStripe\Control\HTTPRequest;
15
use SilverStripe\Control\HTTPResponse_Exception;
16
use SilverStripe\Control\Controller;
17
use SilverStripe\Control\PjaxResponseNegotiator;
18
use SilverStripe\Core\Convert;
19
use SilverStripe\Core\Config\Config;
20
use SilverStripe\Core\Cache;
21
use SilverStripe\Core\ClassInfo;
22
use SilverStripe\Core\Injector\Injector;
23
use SilverStripe\Dev\Deprecation;
24
use SilverStripe\Forms\Form;
25
use SilverStripe\Forms\HiddenField;
26
use SilverStripe\Forms\LiteralField;
27
use SilverStripe\Forms\FormAction;
28
use SilverStripe\Forms\FieldList;
29
use SilverStripe\Forms\DropdownField;
30
use SilverStripe\Forms\PrintableTransformation;
31
use SilverStripe\Forms\HTMLEditor\HTMLEditorConfig;
32
use SilverStripe\Forms\HTMLEditor\HTMLEditorField_Toolbar;
33
use SilverStripe\Forms\Schema\FormSchema;
34
use SilverStripe\i18n\i18n;
35
use SilverStripe\ORM\FieldType\DBHTMLText;
36
use SilverStripe\ORM\Hierarchy\Hierarchy;
37
use SilverStripe\ORM\SS_List;
38
use SilverStripe\ORM\ValidationResult;
39
use SilverStripe\ORM\Versioning\Versioned;
40
use SilverStripe\ORM\DataModel;
41
use SilverStripe\ORM\ValidationException;
42
use SilverStripe\ORM\ArrayList;
43
use SilverStripe\ORM\FieldType\DBField;
44
use SilverStripe\ORM\DataObject;
45
use SilverStripe\ORM\DB;
46
use SilverStripe\Security\SecurityToken;
47
use SilverStripe\Security\Member;
48
use SilverStripe\Security\Permission;
49
use SilverStripe\Security\Security;
50
use SilverStripe\Security\PermissionProvider;
51
use SilverStripe\View\SSViewer;
52
use SilverStripe\View\Requirements;
53
use SilverStripe\View\ArrayData;
54
use ReflectionClass;
55
use InvalidArgumentException;
56
57
use SilverStripe\SiteConfig\SiteConfig;
58
59
/**
60
 * LeftAndMain is the parent class of all the two-pane views in the CMS.
61
 * If you are wanting to add more areas to the CMS, you can do it by subclassing LeftAndMain.
62
 *
63
 * This is essentially an abstract class which should be subclassed.
64
 * See {@link CMSMain} for a good example.
65
 */
66
class LeftAndMain extends Controller implements PermissionProvider {
67
68
    /**
69
     * Form schema header identifier
70
     */
71
    const SCHEMA_HEADER = 'X-Formschema-Request';
72
73
	/**
74
	 * Enable front-end debugging (increases verbosity) in dev mode.
75
	 * Will be ignored in live environments.
76
	 *
77
	 * @var bool
78
	 */
79
	private static $client_debugging = true;
80
81
	/**
82
	 * The current url segment attached to the LeftAndMain instance
83
	 *
84
	 * @config
85
	 * @var string
86
	 */
87
	private static $url_segment;
88
89
	/**
90
	 * @config
91
	 * @var string
92
	 */
93
	private static $url_rule = '/$Action/$ID/$OtherID';
94
95
	/**
96
	 * @config
97
	 * @var string
98
	 */
99
	private static $menu_title;
100
101
	/**
102
	 * @config
103
	 * @var string
104
	 */
105
	private static $menu_icon;
106
107
	/**
108
	 * @config
109
	 * @var int
110
	 */
111
	private static $menu_priority = 0;
112
113
	/**
114
	 * @config
115
	 * @var int
116
	 */
117
	private static $url_priority = 50;
118
119
	/**
120
	 * A subclass of {@link DataObject}.
121
	 *
122
	 * Determines what is managed in this interface, through
123
	 * {@link getEditForm()} and other logic.
124
	 *
125
	 * @config
126
	 * @var string
127
	 */
128
	private static $tree_class = null;
129
130
	/**
131
	 * The url used for the link in the Help tab in the backend
132
	 *
133
	 * @config
134
	 * @var string
135
	 */
136
	private static $help_link = '//userhelp.silverstripe.org/framework/en/3.3';
137
138
	/**
139
	 * @var array
140
	 */
141
	private static $allowed_actions = [
142
		'index',
143
		'save',
144
		'savetreenode',
145
		'getsubtree',
146
		'updatetreenodes',
147
		'printable',
148
		'show',
149
		'EditorToolbar',
150
		'EditForm',
151
		'AddForm',
152
		'batchactions',
153
		'BatchActionsForm',
154
		'schema',
155
	];
156
157
	private static $url_handlers = [
158
		'GET schema/$FormName/$ItemID/$OtherItemID' => 'schema'
159
	];
160
161
	private static $dependencies = [
162
		'FormSchema' => '%$FormSchema'
163
	];
164
165
    /**
166
     * Current form schema helper
167
     *
168
     * @var FormSchema
169
     */
170
    protected $schema = null;
171
172
	/**
173
	 * Assign themes to use for cms
174
	 *
175
	 * @config
176
	 * @var array
177
	 */
178
	private static $admin_themes = [
179
		'silverstripe/framework:/admin/themes/cms-forms',
180
		SSViewer::DEFAULT_THEME,
181
	];
182
183
	/**
184
	 * Codes which are required from the current user to view this controller.
185
	 * If multiple codes are provided, all of them are required.
186
	 * All CMS controllers require "CMS_ACCESS_LeftAndMain" as a baseline check,
187
	 * and fall back to "CMS_ACCESS_<class>" if no permissions are defined here.
188
	 * See {@link canView()} for more details on permission checks.
189
	 *
190
	 * @config
191
	 * @var array
192
	 */
193
	private static $required_permission_codes;
194
195
	/**
196
	 * @config
197
	 * @var String Namespace for session info, e.g. current record.
198
	 * Defaults to the current class name, but can be amended to share a namespace in case
199
	 * controllers are logically bundled together, and mainly separated
200
	 * to achieve more flexible templating.
201
	 */
202
	private static $session_namespace;
203
204
	/**
205
	 * Register additional requirements through the {@link Requirements} class.
206
	 * Used mainly to work around the missing "lazy loading" functionality
207
	 * for getting css/javascript required after an ajax-call (e.g. loading the editform).
208
	 *
209
	 * YAML configuration example:
210
	 * <code>
211
	 * LeftAndMain:
212
	 *   extra_requirements_javascript:
213
	 *     - mysite/javascript/myscript.js
214
	 * </code>
215
	 *
216
	 * @config
217
	 * @var array
218
	 */
219
	private static $extra_requirements_javascript = array();
220
221
	/**
222
	 * YAML configuration example:
223
	 * <code>
224
	 * LeftAndMain:
225
	 *   extra_requirements_css:
226
	 *     - mysite/css/mystyle.css:
227
	*          media: screen
228
	 * </code>
229
	 *
230
	 * @config
231
	 * @var array See {@link extra_requirements_javascript}
232
	 */
233
	private static $extra_requirements_css = array();
234
235
	/**
236
	 * @config
237
	 * @var array See {@link extra_requirements_javascript}
238
	 */
239
	private static $extra_requirements_themedCss = array();
240
241
	/**
242
	 * If true, call a keepalive ping every 5 minutes from the CMS interface,
243
	 * to ensure that the session never dies.
244
	 *
245
	 * @config
246
	 * @var boolean
247
	 */
248
	private static $session_keepalive_ping = true;
249
250
	/**
251
	 * Value of X-Frame-Options header
252
	 *
253
	 * @config
254
	 * @var string
255
	 */
256
	private static $frame_options = 'SAMEORIGIN';
257
258
	/**
259
	 * @var PjaxResponseNegotiator
260
	 */
261
	protected $responseNegotiator;
262
263
	/**
264
	 * Gets the combined configuration of all LeafAndMain subclasses required by the client app.
265
	 *
266
	 * @return array
267
	 *
268
	 * WARNING: Experimental API
269
	 */
270
	public function getCombinedClientConfig() {
271
		$combinedClientConfig = ['sections' => []];
272
		$cmsClassNames = CMSMenu::get_cms_classes('SilverStripe\\Admin\\LeftAndMain', true, CMSMenu::URL_PRIORITY);
273
274
		foreach ($cmsClassNames as $className) {
275
			$combinedClientConfig['sections'][$className] =  Injector::inst()->get($className)->getClientConfig();
276
		}
277
278
		// Pass in base url (absolute and relative)
279
		$combinedClientConfig['baseUrl'] = Director::baseURL();
280
		$combinedClientConfig['absoluteBaseUrl'] = Director::absoluteBaseURL();
281
		$combinedClientConfig['adminUrl'] = AdminRootController::admin_url();
282
283
		// Get "global" CSRF token for use in JavaScript
284
		$token = SecurityToken::inst();
285
		$combinedClientConfig[$token->getName()] = $token->getValue();
286
287
		// Set env
288
		$combinedClientConfig['environment'] = Director::get_environment_type();
289
		$combinedClientConfig['debugging'] = $this->config()->client_debugging;
0 ignored issues
show
Documentation introduced by
The property client_debugging does not exist on object<SilverStripe\Core\Config\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...
290
291
		return Convert::raw2json($combinedClientConfig);
292
	}
293
294
	/**
295
	 * Returns configuration required by the client app.
296
	 *
297
	 * @return array
298
	 *
299
	 * WARNING: Experimental API
300
	 */
301
	public function getClientConfig() {
302
		return [
303
			// Trim leading/trailing slash to make it easier to concatenate URL
304
			// and use in routing definitions.
305
			'url' => trim($this->Link(), '/'),
306
		];
307
	}
308
309
    /**
310
     * Get form schema helper
311
     *
312
     * @return FormSchema
313
     */
314
    public function getFormSchema() {
315
        return $this->schema;
316
    }
317
318
    /**
319
     * Set form schema helper for this controller
320
     *
321
     * @param FormSchema $schema
322
     * @return $this
323
     */
324
    public function setFormSchema(FormSchema $schema) {
325
        $this->schema = $schema;
326
        return $this;
327
    }
328
329
	/**
330
	 * Gets a JSON schema representing the current edit form.
331
	 *
332
	 * WARNING: Experimental API.
333
	 *
334
	 * @param HTTPRequest $request
335
	 * @return HTTPResponse
336
	 */
337
	public function schema($request) {
338
		$formName = $request->param('FormName');
339
		$itemID = $request->param('ItemID');
340
341
		if (!$formName) {
342
			return (new HTTPResponse('Missing request params', 400));
343
		}
344
345
		if(!$this->hasMethod("get{$formName}")) {
346
			return (new HTTPResponse('Form not found', 404));
347
		}
348
349
		if(!$this->hasAction($formName)) {
350
			return (new HTTPResponse('Form not accessible', 401));
351
		}
352
353
		$form = $this->{"get{$formName}"}($itemID);
354
		$schemaID = $request->getURL();
355
        return $this->getSchemaResponse($schemaID, $form);
356
	}
357
358
    /**
359
     * Check if the current request has a X-Formschema-Request header set.
360
     * Used by conditional logic that responds to validation results
361
     *
362
     * @return bool
363
     */
364
	protected function getSchemaRequested() {
365
        $parts = $this->getRequest()->getHeader(static::SCHEMA_HEADER);
366
        return !empty($parts);
367
    }
368
369
	/**
370
	 * Generate schema for the given form based on the X-Formschema-Request header value
371
	 *
372
	 * @param string $schemaID ID for this schema. Required.
373
	 * @param Form $form Required for 'state' or 'schema' response
374
	 * @param ValidationResult $errors Required for 'error' response
375
     * @param array $extraData Any extra data to be merged with the schema response
376
	 * @return HTTPResponse
377
	 */
378
	protected function getSchemaResponse($schemaID, $form = null, ValidationResult $errors = null, $extraData = []) {
379
        $parts = $this->getRequest()->getHeader(static::SCHEMA_HEADER);
380
        $data = $this
381
            ->getFormSchema()
382
            ->getMultipartSchema($parts, $schemaID, $form, $errors);
383
384
        if ($extraData) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $extraData 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...
385
            $data = array_merge($data, $extraData);
386
        }
387
388
        $response = new HTTPResponse(Convert::raw2json($data));
389
        $response->addHeader('Content-Type', 'application/json');
390
        return $response;
391
	}
392
393
	/**
394
	 * Get link to schema url for a given form
395
	 *
396
	 * @param Form $form
397
	 * @return string
398
	 */
399
	protected function getSchemaLinkForForm(Form $form) {
400
		$parts = [$this->Link('schema'), $form->getName()];
401
		if (($record = $form->getRecord()) && $record->isInDB()) {
402
			$parts[] = $record->ID;
403
		} elseif (($data = $form->getData()) && !empty($data['ID'])) {
404
			$parts[] = $data['ID'];
405
		}
406
		return Controller::join_links($parts);
407
	}
408
409
	/**
410
	 * @param Member $member
411
	 * @return boolean
412
	 */
413
	public function canView($member = null) {
414
		if(!$member && $member !== FALSE) $member = Member::currentUser();
415
416
		// cms menus only for logged-in members
417
		if(!$member) return false;
418
419
		// alternative extended checks
420
		if($this->hasMethod('alternateAccessCheck')) {
421
			$alternateAllowed = $this->alternateAccessCheck();
422
			if($alternateAllowed === false) {
423
				return false;
424
			}
425
		}
426
427
		// Check for "CMS admin" permission
428
		if(Permission::checkMember($member, "CMS_ACCESS_LeftAndMain")) {
429
			return true;
430
		}
431
432
		// Check for LeftAndMain sub-class permissions
433
		$codes = $this->getRequiredPermissions();
434
		if($codes === false) { // allow explicit FALSE to disable subclass check
435
			return true;
436
		}
437
		foreach((array)$codes as $code) {
438
			if(!Permission::checkMember($member, $code)) {
439
				return false;
440
			}
441
		}
442
443
		return true;
444
	}
445
446
	/**
447
	 * Get list of required permissions
448
	 *
449
	 * @return array|string|bool Code, array of codes, or false if no permission required
450
	 */
451
	public static function getRequiredPermissions() {
452
		$class = get_called_class();
453
		$code = Config::inst()->get($class, 'required_permission_codes', Config::FIRST_SET);
454
		if ($code === false) {
455
			return false;
456
		}
457
		if ($code) {
458
			return $code;
459
		}
460
		return "CMS_ACCESS_" . $class;
461
	}
462
463
	/**
464
	 * @uses LeftAndMainExtension->init()
465
	 * @uses LeftAndMainExtension->accessedCMS()
466
	 * @uses CMSMenu
467
	 */
468
	protected function init() {
0 ignored issues
show
Coding Style introduced by
init uses the super-global variable $_REQUEST which is generally not recommended.

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

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

// Better
class Router
{
    private $host;

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

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

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

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
469
		parent::init();
470
471
		SSViewer::config()->update('rewrite_hash_links', false);
472
		ContentNegotiator::config()->update('enabled', false);
473
474
		// set language
475
		$member = Member::currentUser();
476
		if(!empty($member->Locale)) {
477
			i18n::set_locale($member->Locale);
478
		}
479
		if(!empty($member->DateFormat)) {
480
			i18n::config()->date_format = $member->DateFormat;
0 ignored issues
show
Documentation introduced by
The property date_format does not exist on object<SilverStripe\Core\Config\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...
481
		}
482
		if(!empty($member->TimeFormat)) {
483
			i18n::config()->time_format = $member->TimeFormat;
0 ignored issues
show
Documentation introduced by
The property time_format does not exist on object<SilverStripe\Core\Config\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...
484
		}
485
486
		// can't be done in cms/_config.php as locale is not set yet
487
		CMSMenu::add_link(
488
			'Help',
489
			_t('LeftAndMain.HELP', 'Help', 'Menu title'),
490
			$this->config()->help_link,
491
			-2,
492
			array(
493
				'target' => '_blank'
494
			)
495
		);
496
497
		// Allow customisation of the access check by a extension
498
		// Also all the canView() check to execute Controller::redirect()
499
		if(!$this->canView() && !$this->getResponse()->isFinished()) {
500
			// When access /admin/, we should try a redirect to another part of the admin rather than be locked out
501
			$menu = $this->MainMenu();
502
			foreach($menu as $candidate) {
503
				if(
504
					$candidate->Link &&
505
					$candidate->Link != $this->Link()
506
					&& $candidate->MenuItem->controller
507
					&& singleton($candidate->MenuItem->controller)->canView()
508
				) {
509
					$this->redirect($candidate->Link);
510
					return;
511
				}
512
			}
513
514
			if(Member::currentUser()) {
515
				Session::set("BackURL", null);
516
			}
517
518
			// if no alternate menu items have matched, return a permission error
519
			$messageSet = array(
520
				'default' => _t(
521
					'LeftAndMain.PERMDEFAULT',
522
					"You must be logged in to access the administration area; please enter your credentials below."
523
				),
524
				'alreadyLoggedIn' => _t(
525
					'LeftAndMain.PERMALREADY',
526
					"I'm sorry, but you can't access that part of the CMS.  If you want to log in as someone else, do"
527
					. " so below."
528
				),
529
				'logInAgain' => _t(
530
					'LeftAndMain.PERMAGAIN',
531
					"You have been logged out of the CMS.  If you would like to log in again, enter a username and"
532
					. " password below."
533
				),
534
			);
535
536
			Security::permissionFailure($this, $messageSet);
537
			return;
538
		}
539
540
		// Don't continue if there's already been a redirection request.
541
		if($this->redirectedTo()) {
542
			return;
543
		}
544
545
		// Audit logging hook
546
		if(empty($_REQUEST['executeForm']) && !$this->getRequest()->isAjax()) $this->extend('accessedCMS');
547
548
		// Set the members html editor config
549
		if(Member::currentUser()) {
550
			HTMLEditorConfig::set_active_identifier(Member::currentUser()->getHtmlEditorConfigForCMS());
551
		}
552
553
		// Set default values in the config if missing.  These things can't be defined in the config
554
		// file because insufficient information exists when that is being processed
555
		$htmlEditorConfig = HTMLEditorConfig::get_active();
556
		$htmlEditorConfig->setOption('language', i18n::get_tinymce_lang());
557
558
		Requirements::customScript("
559
			window.ss = window.ss || {};
560
			window.ss.config = " . $this->getCombinedClientConfig() . ";
561
		");
562
563
		Requirements::javascript(FRAMEWORK_ADMIN_DIR . '/client/dist/js/vendor.js');
564
		Requirements::javascript(FRAMEWORK_ADMIN_DIR . '/client/dist/js/bundle.js');
565
		Requirements::css(ltrim(FRAMEWORK_ADMIN_DIR . '/client/dist/styles/bundle.css', '/'));
566
567
		Requirements::add_i18n_javascript(ltrim(FRAMEWORK_DIR . '/client/lang', '/'), false, true);
568
		Requirements::add_i18n_javascript(FRAMEWORK_ADMIN_DIR . '/client/lang', false, true);
569
570
		if ($this->config()->session_keepalive_ping) {
571
			Requirements::javascript(FRAMEWORK_ADMIN_DIR . '/client/dist/js/LeftAndMain.Ping.js');
572
		}
573
574
		if (Director::isDev()) {
575
			// TODO Confuses jQuery.ondemand through document.write()
576
			Requirements::javascript(ADMIN_THIRDPARTY_DIR . '/jquery-entwine/src/jquery.entwine.inspector.js');
577
			Requirements::javascript(FRAMEWORK_ADMIN_DIR . '/client/dist/js/leaktools.js');
578
		}
579
580
		// Custom requirements
581
		$extraJs = $this->stat('extra_requirements_javascript');
582
583
		if($extraJs) {
584
			foreach($extraJs as $file => $config) {
585
				if(is_numeric($file)) {
586
					$file = $config;
587
				}
588
589
				Requirements::javascript($file);
590
			}
591
		}
592
593
		$extraCss = $this->stat('extra_requirements_css');
594
595
		if($extraCss) {
596
			foreach($extraCss as $file => $config) {
597
				if(is_numeric($file)) {
598
					$file = $config;
599
					$config = array();
600
				}
601
602
				Requirements::css($file, isset($config['media']) ? $config['media'] : null);
603
			}
604
		}
605
606
		$extraThemedCss = $this->stat('extra_requirements_themedCss');
607
608
		if($extraThemedCss) {
609
			foreach ($extraThemedCss as $file => $config) {
610
				if(is_numeric($file)) {
611
					$file = $config;
612
					$config = array();
613
				}
614
615
				Requirements::themedCSS($file, isset($config['media']) ? $config['media'] : null);
616
			}
617
		}
618
619
		$dummy = null;
620
		$this->extend('init', $dummy);
621
622
		// Assign default cms theme and replace user-specified themes
623
		SSViewer::set_themes($this->config()->admin_themes);
624
625
		//set the reading mode for the admin to stage
626
		Versioned::set_stage(Versioned::DRAFT);
627
	}
628
629
	public function handleRequest(HTTPRequest $request, DataModel $model = null) {
630
		try {
631
			$response = parent::handleRequest($request, $model);
0 ignored issues
show
Bug introduced by
It seems like $model defined by parameter $model on line 629 can be null; however, SilverStripe\Control\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...
632
		} catch(ValidationException $e) {
633
			// Nicer presentation of model-level validation errors
634
			$msgs = _t('LeftAndMain.ValidationError', 'Validation error') . ': '
635
				. $e->getMessage();
636
			$e = new HTTPResponse_Exception($msgs, 403);
637
			$errorResponse = $e->getResponse();
638
			$errorResponse->addHeader('Content-Type', 'text/plain');
639
			$errorResponse->addHeader('X-Status', rawurlencode($msgs));
640
			$e->setResponse($errorResponse);
641
			throw $e;
642
		}
643
644
		$title = $this->Title();
645
		if(!$response->getHeader('X-Controller')) $response->addHeader('X-Controller', $this->class);
646
		if(!$response->getHeader('X-Title')) $response->addHeader('X-Title', urlencode($title));
647
648
		// Prevent clickjacking, see https://developer.mozilla.org/en-US/docs/HTTP/X-Frame-Options
649
		$originalResponse = $this->getResponse();
650
		$originalResponse->addHeader('X-Frame-Options', $this->config()->frame_options);
651
		$originalResponse->addHeader('Vary', 'X-Requested-With');
652
653
		return $response;
654
	}
655
656
	/**
657
	 * Overloaded redirection logic to trigger a fake redirect on ajax requests.
658
	 * While this violates HTTP principles, its the only way to work around the
659
	 * fact that browsers handle HTTP redirects opaquely, no intervention via JS is possible.
660
	 * In isolation, that's not a problem - but combined with history.pushState()
661
	 * it means we would request the same redirection URL twice if we want to update the URL as well.
662
	 * See LeftAndMain.js for the required jQuery ajaxComplete handlers.
663
	 *
664
	 * @param string $url
665
	 * @param int $code
666
	 * @return HTTPResponse|string
667
	 */
668
	public function redirect($url, $code=302) {
669
		if($this->getRequest()->isAjax()) {
670
			$response = $this->getResponse();
671
			$response->addHeader('X-ControllerURL', $url);
672
			if($this->getRequest()->getHeader('X-Pjax') && !$response->getHeader('X-Pjax')) {
673
				$response->addHeader('X-Pjax', $this->getRequest()->getHeader('X-Pjax'));
674
			}
675
			$newResponse = new LeftAndMain_HTTPResponse(
676
				$response->getBody(),
677
				$response->getStatusCode(),
678
				$response->getStatusDescription()
679
			);
680
			foreach($response->getHeaders() as $k => $v) {
681
				$newResponse->addHeader($k, $v);
682
			}
683
			$newResponse->setIsFinished(true);
684
			$this->setResponse($newResponse);
685
			return ''; // Actual response will be re-requested by client
686
		} else {
687
			parent::redirect($url, $code);
688
		}
689
	}
690
691
	/**
692
	 * @param HTTPRequest $request
693
	 * @return HTTPResponse
694
	 */
695
	public function index($request) {
696
		return $this->getResponseNegotiator()->respond($request);
697
	}
698
699
	/**
700
	 * If this is set to true, the "switchView" context in the
701
	 * template is shown, with links to the staging and publish site.
702
	 *
703
	 * @return boolean
704
	 */
705
	public function ShowSwitchView() {
706
		return false;
707
	}
708
709
710
	//------------------------------------------------------------------------------------------//
711
	// Main controllers
712
713
	/**
714
	 * You should implement a Link() function in your subclass of LeftAndMain,
715
	 * to point to the URL of that particular controller.
716
	 *
717
	 * @param string $action
718
	 * @return string
719
	 */
720
	public function Link($action = null) {
721
		// Handle missing url_segments
722
		if($this->config()->url_segment) {
723
			$segment = $this->config()->get('url_segment', Config::FIRST_SET);
724
		} else {
725
			$segment = $this->class;
726
		};
727
728
		$link = Controller::join_links(
729
			AdminRootController::admin_url(),
730
			$segment,
731
			'/', // trailing slash needed if $action is null!
732
			"$action"
733
		);
734
		$this->extend('updateLink', $link);
735
		return $link;
736
	}
737
738
	/**
739
	 * @deprecated 5.0
740
	 */
741
	public static function menu_title_for_class($class) {
742
		Deprecation::notice('5.0', 'Use menu_title() instead');
743
		return static::menu_title($class, false);
744
	}
745
746
	/**
747
	 * Get menu title for this section (translated)
748
	 *
749
	 * @param string $class Optional class name if called on LeftAndMain directly
750
	 * @param bool $localise Determine if menu title should be localised via i18n.
751
	 * @return string Menu title for the given class
752
	 */
753
	public static function menu_title($class = null, $localise = true) {
754
		if($class && is_subclass_of($class, __CLASS__)) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $class of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

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

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

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

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

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

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

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

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
759
			$class = get_called_class();
760
		}
761
762
		// Get default class title
763
		$title = Config::inst()->get($class, 'menu_title', Config::FIRST_SET);
764
		if(!$title) {
765
			$title = preg_replace('/Admin$/', '', $class);
766
		}
767
768
		// Check localisation
769
		if(!$localise) {
770
			return $title;
771
		}
772
		return i18n::_t("{$class}.MENUTITLE", $title);
773
	}
774
775
	/**
776
	 * Return styling for the menu icon, if a custom icon is set for this class
777
	 *
778
	 * Example: static $menu-icon = '/path/to/image/';
779
	 * @param string $class
780
	 * @return string
781
	 */
782
	public static function menu_icon_for_class($class) {
783
		$icon = Config::inst()->get($class, 'menu_icon', Config::FIRST_SET);
784
		if (!empty($icon)) {
785
			$class = strtolower(Convert::raw2htmlname(str_replace('\\', '-', $class)));
786
			return ".icon.icon-16.icon-{$class} { background-image: url('{$icon}'); } ";
787
		}
788
		return '';
789
	}
790
791
	/**
792
	 * @param HTTPRequest $request
793
	 * @return HTTPResponse
794
	 * @throws HTTPResponse_Exception
795
	 */
796
	public function show($request) {
797
		// TODO Necessary for TableListField URLs to work properly
798
		if($request->param('ID')) $this->setCurrentPageID($request->param('ID'));
799
		return $this->getResponseNegotiator()->respond($request);
800
	}
801
802
	/**
803
	 * Caution: Volatile API.
804
	 *
805
	 * @return PjaxResponseNegotiator
806
	 */
807
	public function getResponseNegotiator() {
808
		if(!$this->responseNegotiator) {
809
			$controller = $this;
810
			$this->responseNegotiator = new PjaxResponseNegotiator(
811
				array(
812
					'CurrentForm' => function() use(&$controller) {
813
						return $controller->getEditForm()->forTemplate();
814
					},
815
					'Content' => function() use(&$controller) {
816
						return $controller->renderWith($controller->getTemplatesWithSuffix('_Content'));
817
					},
818
					'Breadcrumbs' => function() use (&$controller) {
819
						return $controller->renderWith([
820
							'type' => 'Includes',
821
							'SilverStripe\\Admin\\CMSBreadcrumbs'
822
						]);
823
					},
824
					'default' => function() use(&$controller) {
825
						return $controller->renderWith($controller->getViewer('show'));
826
					}
827
				),
828
				$this->getResponse()
829
			);
830
		}
831
		return $this->responseNegotiator;
832
	}
833
834
	//------------------------------------------------------------------------------------------//
835
	// Main UI components
836
837
	/**
838
	 * Returns the main menu of the CMS.  This is also used by init()
839
	 * to work out which sections the user has access to.
840
	 *
841
	 * @param bool $cached
842
	 * @return SS_List
843
	 */
844
	public function MainMenu($cached = true) {
845
		if(!isset($this->_cache_MainMenu) || !$cached) {
846
			// Don't accidentally return a menu if you're not logged in - it's used to determine access.
847
			if(!Member::currentUser()) return new ArrayList();
848
849
			// Encode into DO set
850
			$menu = new ArrayList();
851
			$menuItems = CMSMenu::get_viewable_menu_items();
852
853
			// extra styling for custom menu-icons
854
			$menuIconStyling = '';
855
856
			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...
857
				/** @var CMSMenuItem $menuItem */
858
				foreach($menuItems as $code => $menuItem) {
859
					// alternate permission checks (in addition to LeftAndMain->canView())
860
					if(
861
						isset($menuItem->controller)
862
						&& $this->hasMethod('alternateMenuDisplayCheck')
863
						&& !$this->alternateMenuDisplayCheck($menuItem->controller)
864
					) {
865
						continue;
866
					}
867
868
					$linkingmode = "link";
869
870
					if($menuItem->controller && get_class($this) == $menuItem->controller) {
871
						$linkingmode = "current";
872
					} else if(strpos($this->Link(), $menuItem->url) !== false) {
873
						if($this->Link() == $menuItem->url) {
874
							$linkingmode = "current";
875
876
						// default menu is the one with a blank {@link url_segment}
877
						} else if(singleton($menuItem->controller)->stat('url_segment') == '') {
878
							if($this->Link() == AdminRootController::admin_url()) {
879
								$linkingmode = "current";
880
							}
881
882
						} else {
883
							$linkingmode = "current";
884
						}
885
					}
886
887
					// already set in CMSMenu::populate_menu(), but from a static pre-controller
888
					// context, so doesn't respect the current user locale in _t() calls - as a workaround,
889
					// we simply call LeftAndMain::menu_title() again
890
					// if we're dealing with a controller
891
					if($menuItem->controller) {
892
						$title = LeftAndMain::menu_title($menuItem->controller);
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

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

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

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

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

Loading history...
893
					} else {
894
						$title = $menuItem->title;
895
					}
896
897
					// Provide styling for custom $menu-icon. Done here instead of in
898
					// CMSMenu::populate_menu(), because the icon is part of
899
					// the CMS right pane for the specified class as well...
900
					if($menuItem->controller) {
901
						$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...
902
						if (!empty($menuIcon)) {
903
							$menuIconStyling .= $menuIcon;
904
						}
905
					}
906
907
					$menu->push(new ArrayData(array(
908
						"MenuItem" => $menuItem,
909
						"AttributesHTML" => $menuItem->getAttributesHTML(),
910
						"Title" => Convert::raw2xml($title),
911
						"Code" => $code,
912
						"Icon" => strtolower($code),
913
						"Link" => $menuItem->url,
914
						"LinkingMode" => $linkingmode
915
					)));
916
				}
917
			}
918
			if ($menuIconStyling) Requirements::customCSS($menuIconStyling);
919
920
			$this->_cache_MainMenu = $menu;
0 ignored issues
show
Documentation introduced by
The property _cache_MainMenu does not exist on object<SilverStripe\Admin\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...
921
		}
922
923
		return $this->_cache_MainMenu;
924
	}
925
926
	public function Menu() {
927
		return $this->renderWith($this->getTemplatesWithSuffix('_Menu'));
928
	}
929
930
	/**
931
	 * @todo Wrap in CMSMenu instance accessor
932
	 * @return ArrayData A single menu entry (see {@link MainMenu})
933
	 */
934
	public function MenuCurrentItem() {
935
		$items = $this->MainMenu();
936
		return $items->find('LinkingMode', 'current');
937
	}
938
939
	/**
940
	 * Return a list of appropriate templates for this class, with the given suffix using
941
	 * {@link SSViewer::get_templates_by_class()}
942
	 *
943
	 * @param string $suffix
944
	 * @return array
945
	 */
946
	public function getTemplatesWithSuffix($suffix) {
947
		$templates = SSViewer::get_templates_by_class(get_class($this), $suffix, __CLASS__);
948
		return SSViewer::chooseTemplate($templates);
949
	}
950
951
	public function Content() {
952
		return $this->renderWith($this->getTemplatesWithSuffix('_Content'));
953
	}
954
955
	/**
956
	 * Render $PreviewPanel content
957
	 *
958
	 * @return DBHTMLText
959
	 */
960
	public function PreviewPanel() {
961
		$template = $this->getTemplatesWithSuffix('_PreviewPanel');
962
		// Only render sections with preview panel
963
		if ($template) {
964
			return $this->renderWith($template);
965
		}
966
	}
967
968
	public function getRecord($id) {
969
		$className = $this->stat('tree_class');
970
		if($className && $id instanceof $className) {
971
			return $id;
972
		} else if($className && $id == 'root') {
973
			return singleton($className);
974
		} else if($className && is_numeric($id)) {
975
			return DataObject::get_by_id($className, $id);
976
		} else {
977
			return false;
978
		}
979
	}
980
981
	/**
982
	 * @param bool $unlinked
983
	 * @return ArrayList
984
	 */
985
	public function Breadcrumbs($unlinked = false) {
986
		$items = new ArrayList(array(
987
			new ArrayData(array(
988
				'Title' => $this->menu_title(),
989
				'Link' => ($unlinked) ? false : $this->Link()
990
			))
991
		));
992
		$record = $this->currentPage();
993
		if($record && $record->exists()) {
994
			if($record->hasExtension(Hierarchy::class)) {
995
				$ancestors = $record->getAncestors();
996
				$ancestors = new ArrayList(array_reverse($ancestors->toArray()));
997
				$ancestors->push($record);
998
				foreach($ancestors as $ancestor) {
999
					$items->push(new ArrayData(array(
1000
						'Title' => ($ancestor->MenuTitle) ? $ancestor->MenuTitle : $ancestor->Title,
1001
						'Link' => ($unlinked) ? false : Controller::join_links($this->Link('show'), $ancestor->ID)
1002
					)));
1003
				}
1004
			} else {
1005
				$items->push(new ArrayData(array(
1006
					'Title' => ($record->MenuTitle) ? $record->MenuTitle : $record->Title,
1007
					'Link' => ($unlinked) ? false : Controller::join_links($this->Link('show'), $record->ID)
1008
				)));
1009
			}
1010
		}
1011
1012
		return $items;
1013
	}
1014
1015
	/**
1016
	 * @return String HTML
1017
	 */
1018
	public function SiteTreeAsUL() {
1019
		$html = $this->getSiteTreeFor($this->stat('tree_class'));
1020
		$this->extend('updateSiteTreeAsUL', $html);
1021
		return $html;
1022
	}
1023
1024
	/**
1025
	 * Gets the current search filter for this request, if available
1026
	 *
1027
	 * @throws InvalidArgumentException
1028
	 * @return LeftAndMain_SearchFilter
1029
	 */
1030
	protected function getSearchFilter() {
1031
		// Check for given FilterClass
1032
		$params = $this->getRequest()->getVar('q');
1033
		if(empty($params['FilterClass'])) {
1034
			return null;
1035
		}
1036
1037
		// Validate classname
1038
		$filterClass = $params['FilterClass'];
1039
		$filterInfo = new ReflectionClass($filterClass);
1040
		if(!$filterInfo->implementsInterface('SilverStripe\\Admin\\LeftAndMain_SearchFilter')) {
1041
			throw new InvalidArgumentException(sprintf('Invalid filter class passed: %s', $filterClass));
1042
		}
1043
1044
		return Injector::inst()->createWithArgs($filterClass, array($params));
1045
	}
1046
1047
	/**
1048
	 * Get a site tree HTML listing which displays the nodes under the given criteria.
1049
	 *
1050
	 * @param string $className The class of the root object
1051
	 * @param string $rootID The ID of the root object.  If this is null then a complete tree will be
1052
	 *  shown
1053
	 * @param string $childrenMethod The method to call to get the children of the tree. For example,
1054
	 *  Children, AllChildrenIncludingDeleted, or AllHistoricalChildren
1055
	 * @param string $numChildrenMethod
1056
	 * @param callable $filterFunction
1057
	 * @param int $nodeCountThreshold
1058
	 * @return string Nested unordered list with links to each page
1059
	 */
1060
	public function getSiteTreeFor($className, $rootID = null, $childrenMethod = null, $numChildrenMethod = null,
1061
			$filterFunction = null, $nodeCountThreshold = 30) {
1062
1063
		// Filter criteria
1064
		$filter = $this->getSearchFilter();
1065
1066
		// Default childrenMethod and numChildrenMethod
1067
		if(!$childrenMethod) $childrenMethod = ($filter && $filter->getChildrenMethod())
0 ignored issues
show
Bug Best Practice introduced by
The expression $childrenMethod of type string|null is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

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

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

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

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
1068
			? $filter->getChildrenMethod()
1069
			: 'AllChildrenIncludingDeleted';
1070
1071
		if(!$numChildrenMethod) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $numChildrenMethod of type string|null is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

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

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

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

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
1072
			$numChildrenMethod = 'numChildren';
1073
			if($filter && $filter->getNumChildrenMethod()) {
1074
				$numChildrenMethod = $filter->getNumChildrenMethod();
1075
			}
1076
		}
1077
		if(!$filterFunction && $filter) {
1078
			$filterFunction = function($node) use($filter) {
1079
				return $filter->isPageIncluded($node);
1080
			};
1081
		}
1082
1083
		// Get the tree root
1084
		$record = ($rootID) ? $this->getRecord($rootID) : null;
1085
		$obj = $record ? $record : singleton($className);
1086
1087
		// Get the current page
1088
		// NOTE: This *must* be fetched before markPartialTree() is called, as this
1089
		// causes the Hierarchy::$marked cache to be flushed (@see CMSMain::getRecord)
1090
		// which means that deleted pages stored in the marked tree would be removed
1091
		$currentPage = $this->currentPage();
1092
1093
		// Mark the nodes of the tree to return
1094
		if ($filterFunction) $obj->setMarkingFilterFunction($filterFunction);
1095
1096
		$obj->markPartialTree($nodeCountThreshold, $this, $childrenMethod, $numChildrenMethod);
1097
1098
		// Ensure current page is exposed
1099
		if($currentPage) $obj->markToExpose($currentPage);
1100
1101
		// NOTE: SiteTree/CMSMain coupling :-(
1102
		if(class_exists('SilverStripe\\CMS\\Model\\SiteTree')) {
1103
			SiteTree::prepopulate_permission_cache(
1104
				'CanEditType',
1105
				$obj->markedNodeIDs(),
1106
				'SilverStripe\\CMS\\Model\\SiteTree::can_edit_multiple'
1107
			);
1108
		}
1109
1110
		// getChildrenAsUL is a flexible and complex way of traversing the tree
1111
		$controller = $this;
1112
		$recordController = ($this->stat('tree_class') == 'SilverStripe\\CMS\\Model\\SiteTree')
1113
			?  CMSPageEditController::singleton()
1114
			: $this;
1115
		$titleFn = function(&$child, $numChildrenMethod) use(&$controller, &$recordController, $filter) {
1116
			$link = Controller::join_links($recordController->Link("show"), $child->ID);
1117
			$node = LeftAndMain_TreeNode::create($child, $link, $controller->isCurrentPage($child), $numChildrenMethod, $filter);
1118
			return $node->forTemplate();
1119
		};
1120
1121
		// Limit the amount of nodes shown for performance reasons.
1122
		// Skip the check if we're filtering the tree, since its not clear how many children will
1123
		// match the filter criteria until they're queried (and matched up with previously marked nodes).
1124
		$nodeThresholdLeaf = Config::inst()->get(Hierarchy::class, 'node_threshold_leaf');
1125
		if($nodeThresholdLeaf && !$filterFunction) {
1126
			$nodeCountCallback = function($parent, $numChildren) use(&$controller, $className, $nodeThresholdLeaf) {
1127
				if ($className !== 'SilverStripe\\CMS\\Model\\SiteTree'
1128
					|| !$parent->ID
1129
					|| $numChildren >= $nodeThresholdLeaf
1130
				) {
1131
					return null;
1132
				}
1133
					return sprintf(
1134
						'<ul><li class="readonly"><span class="item">'
1135
							. '%s (<a href="%s" class="cms-panel-link" data-pjax-target="Content">%s</a>)'
1136
							. '</span></li></ul>',
1137
						_t('LeftAndMain.TooManyPages', 'Too many pages'),
1138
						Controller::join_links(
1139
							$controller->LinkWithSearch($controller->Link()), '
1140
							?view=list&ParentID=' . $parent->ID
1141
						),
1142
						_t(
1143
							'LeftAndMain.ShowAsList',
1144
							'show as list',
1145
							'Show large amount of pages in list instead of tree view'
1146
						)
1147
					);
1148
			};
1149
		} else {
1150
			$nodeCountCallback = null;
1151
		}
1152
1153
		// If the amount of pages exceeds the node thresholds set, use the callback
1154
		$html = null;
1155
		if($obj->ParentID && $nodeCountCallback) {
1156
			$html = $nodeCountCallback($obj, $obj->$numChildrenMethod());
1157
		}
1158
1159
		// Otherwise return the actual tree (which might still filter leaf thresholds on children)
1160
		if(!$html) {
1161
			$html = $obj->getChildrenAsUL(
1162
				"",
1163
				$titleFn,
1164
				CMSPagesController::singleton(),
1165
				true,
1166
				$childrenMethod,
1167
				$numChildrenMethod,
1168
				$nodeCountThreshold,
1169
				$nodeCountCallback
1170
			);
1171
		}
1172
1173
		// Wrap the root if needs be.
1174
		if(!$rootID) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $rootID of type string|null is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

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

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

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

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
1175
			$rootLink = $this->Link('show') . '/root';
1176
1177
			// This lets us override the tree title with an extension
1178
			if($this->hasMethod('getCMSTreeTitle') && $customTreeTitle = $this->getCMSTreeTitle()) {
1179
				$treeTitle = $customTreeTitle;
1180
			} elseif(class_exists('SilverStripe\\SiteConfig\\SiteConfig')) {
1181
				$siteConfig = SiteConfig::current_site_config();
1182
				$treeTitle =  Convert::raw2xml($siteConfig->Title);
1183
			} else {
1184
				$treeTitle = '...';
1185
			}
1186
1187
			$html = "<ul><li id=\"record-0\" data-id=\"0\" class=\"Root nodelete\"><strong>$treeTitle</strong>"
1188
				. $html . "</li></ul>";
1189
		}
1190
1191
		return $html;
1192
	}
1193
1194
	/**
1195
	 * Get a subtree underneath the request param 'ID'.
1196
	 * If ID = 0, then get the whole tree.
1197
	 *
1198
	 * @param HTTPRequest $request
1199
	 * @return string
1200
	 */
1201
	public function getsubtree($request) {
1202
		$html = $this->getSiteTreeFor(
1203
			$this->stat('tree_class'),
1204
			$request->getVar('ID'),
1205
			null,
1206
			null,
1207
			null,
1208
			$request->getVar('minNodeCount')
1209
		);
1210
1211
		// Trim off the outer tag
1212
		$html = preg_replace('/^[\s\t\r\n]*<ul[^>]*>/','', $html);
1213
		$html = preg_replace('/<\/ul[^>]*>[\s\t\r\n]*$/','', $html);
1214
1215
		return $html;
1216
	}
1217
1218
	/**
1219
	 * Allows requesting a view update on specific tree nodes.
1220
	 * Similar to {@link getsubtree()}, but doesn't enforce loading
1221
	 * all children with the node. Useful to refresh views after
1222
	 * state modifications, e.g. saving a form.
1223
	 *
1224
	 * @param HTTPRequest $request
1225
	 * @return string JSON
1226
	 */
1227
	public function updatetreenodes($request) {
1228
		$data = array();
1229
		$ids = explode(',', $request->getVar('ids'));
1230
		foreach($ids as $id) {
1231
			if($id === "") continue; // $id may be a blank string, which is invalid and should be skipped over
1232
1233
			$record = $this->getRecord($id);
1234
			if(!$record) continue; // In case a page is no longer available
1235
			$recordController = ($this->stat('tree_class') == 'SilverStripe\\CMS\\Model\\SiteTree')
1236
				? CMSPageEditController::singleton()
1237
				: $this;
1238
1239
			// Find the next & previous nodes, for proper positioning (Sort isn't good enough - it's not a raw offset)
1240
			// TODO: These methods should really be in hierarchy - for a start it assumes Sort exists
1241
			$next = $prev = null;
1242
1243
			$className = $this->stat('tree_class');
1244
			$next = DataObject::get($className)
1245
				->filter('ParentID', $record->ParentID)
1246
				->filter('Sort:GreaterThan', $record->Sort)
1247
				->first();
1248
1249
			if (!$next) {
1250
				$prev = DataObject::get($className)
1251
					->filter('ParentID', $record->ParentID)
1252
					->filter('Sort:LessThan', $record->Sort)
1253
					->reverse()
1254
					->first();
1255
			}
1256
1257
			$link = Controller::join_links($recordController->Link("show"), $record->ID);
1258
			$html = LeftAndMain_TreeNode::create($record, $link, $this->isCurrentPage($record))
1259
				->forTemplate() . '</li>';
1260
1261
			$data[$id] = array(
1262
				'html' => $html,
1263
				'ParentID' => $record->ParentID,
1264
				'NextID' => $next ? $next->ID : null,
1265
				'PrevID' => $prev ? $prev->ID : null
1266
			);
1267
		}
1268
		$this->getResponse()->addHeader('Content-Type', 'text/json');
1269
		return Convert::raw2json($data);
1270
	}
1271
1272
	/**
1273
	 * Save  handler
1274
	 *
1275
	 * @param array $data
1276
	 * @param Form $form
1277
	 * @return HTTPResponse
1278
	 */
1279
	public function save($data, $form) {
1280
		$request = $this->getRequest();
1281
		$className = $this->stat('tree_class');
1282
1283
		// Existing or new record?
1284
		$id = $data['ID'];
1285
		if(is_numeric($id) && $id > 0) {
1286
			$record = DataObject::get_by_id($className, $id);
1287
			if($record && !$record->canEdit()) {
1288
				return Security::permissionFailure($this);
1289
			}
1290
			if(!$record || !$record->ID) {
1291
				$this->httpError(404, "Bad record ID #" . (int)$id);
1292
			}
1293
		} else {
1294
			if(!singleton($this->stat('tree_class'))->canCreate()) {
1295
				return Security::permissionFailure($this);
1296
			}
1297
			$record = $this->getNewItem($id, false);
1298
		}
1299
1300
		// save form data into record
1301
		$form->saveInto($record, true);
1302
		$record->write();
1303
		$this->extend('onAfterSave', $record);
1304
		$this->setCurrentPageID($record->ID);
1305
1306
		$message = _t('LeftAndMain.SAVEDUP', 'Saved.');
1307
        if($this->getSchemaRequested()) {
1308
			$schemaId = Controller::join_links($this->Link('schema/DetailEditForm'), $id);
1309
			// Ensure that newly created records have all their data loaded back into the form.
1310
			$form->loadDataFrom($record);
1311
			$form->setMessage($message, 'good');
1312
            $response = $this->getSchemaResponse($schemaId, $form);
1313
		} else {
1314
			$response = $this->getResponseNegotiator()->respond($request);
1315
		}
1316
1317
		$response->addHeader('X-Status', rawurlencode($message));
1318
		return $response;
1319
	}
1320
1321
	/**
1322
	 * Create new item.
1323
	 *
1324
	 * @param string|int $id
1325
	 * @param bool $setID
1326
	 * @return DataObject
1327
	 */
1328
	public function getNewItem($id, $setID = true) {
1329
		$class = $this->stat('tree_class');
1330
		$object = Injector::inst()->create($class);
1331
		if($setID) {
1332
			$object->ID = $id;
1333
		}
1334
		return $object;
1335
	}
1336
1337
	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...
1338
		$className = $this->stat('tree_class');
1339
1340
		$id = $data['ID'];
1341
		$record = DataObject::get_by_id($className, $id);
1342
		if($record && !$record->canDelete()) return Security::permissionFailure();
1343
		if(!$record || !$record->ID) $this->httpError(404, "Bad record ID #" . (int)$id);
1344
1345
		$record->delete();
1346
1347
		$this->getResponse()->addHeader('X-Status', rawurlencode(_t('LeftAndMain.DELETED', 'Deleted.')));
1348
		return $this->getResponseNegotiator()->respond(
1349
			$this->getRequest(),
1350
			array('currentform' => array($this, 'EmptyForm'))
1351
		);
1352
	}
1353
1354
	/**
1355
	 * Update the position and parent of a tree node.
1356
	 * Only saves the node if changes were made.
1357
	 *
1358
	 * Required data:
1359
	 * - 'ID': The moved node
1360
	 * - 'ParentID': New parent relation of the moved node (0 for root)
1361
	 * - 'SiblingIDs': Array of all sibling nodes to the moved node (incl. the node itself).
1362
	 *   In case of a 'ParentID' change, relates to the new siblings under the new parent.
1363
	 *
1364
	 * @param HTTPRequest $request
1365
	 * @return HTTPResponse JSON string with a
1366
	 * @throws HTTPResponse_Exception
1367
	 */
1368
	public function savetreenode($request) {
1369
		if (!SecurityToken::inst()->checkRequest($request)) {
1370
			return $this->httpError(400);
1371
		}
1372
		if (!Permission::check('SITETREE_REORGANISE') && !Permission::check('ADMIN')) {
1373
			$this->getResponse()->setStatusCode(
1374
				403,
1375
				_t('LeftAndMain.CANT_REORGANISE',
1376
					"You do not have permission to rearange the site tree. Your change was not saved.")
1377
			);
1378
			return;
1379
		}
1380
1381
		$className = $this->stat('tree_class');
1382
		$statusUpdates = array('modified'=>array());
1383
		$id = $request->requestVar('ID');
1384
		$parentID = $request->requestVar('ParentID');
1385
1386
		if($className == 'SilverStripe\\CMS\\Model\\SiteTree' && $page = DataObject::get_by_id('Page', $id)){
1387
			$root = $page->getParentType();
1388
			if(($parentID == '0' || $root == 'root') && !SiteConfig::current_site_config()->canCreateTopLevel()){
1389
				$this->getResponse()->setStatusCode(
1390
					403,
1391
					_t('LeftAndMain.CANT_REORGANISE',
1392
						"You do not have permission to alter Top level pages. Your change was not saved.")
1393
					);
1394
				return;
1395
			}
1396
		}
1397
1398
		$siblingIDs = $request->requestVar('SiblingIDs');
1399
		$statusUpdates = array('modified'=>array());
1400
		if(!is_numeric($id) || !is_numeric($parentID)) throw new InvalidArgumentException();
1401
1402
		$node = DataObject::get_by_id($className, $id);
1403
		if($node && !$node->canEdit()) return Security::permissionFailure($this);
1404
1405
		if(!$node) {
1406
			$this->getResponse()->setStatusCode(
1407
				500,
1408
				_t('LeftAndMain.PLEASESAVE',
1409
					"Please Save Page: This page could not be updated because it hasn't been saved yet."
1410
				)
1411
			);
1412
			return;
1413
		}
1414
1415
		// Update hierarchy (only if ParentID changed)
1416
		if($node->ParentID != $parentID) {
1417
			$node->ParentID = (int)$parentID;
0 ignored issues
show
Documentation introduced by
The property ParentID does not exist on object<SilverStripe\ORM\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...
1418
			$node->write();
1419
1420
			$statusUpdates['modified'][$node->ID] = array(
1421
				'TreeTitle'=>$node->TreeTitle
1422
			);
1423
1424
			// Update all dependent pages
1425
			if(class_exists('SilverStripe\\CMS\\Model\\VirtualPage')) {
1426
				$virtualPages = VirtualPage::get()->filter("CopyContentFromID", $node->ID);
1427
				foreach($virtualPages as $virtualPage) {
1428
					$statusUpdates['modified'][$virtualPage->ID] = array(
1429
						'TreeTitle' => $virtualPage->TreeTitle()
1430
					);
1431
				}
1432
			}
1433
1434
			$this->getResponse()->addHeader('X-Status',
1435
				rawurlencode(_t('LeftAndMain.REORGANISATIONSUCCESSFUL', 'Reorganised the site tree successfully.')));
1436
		}
1437
1438
		// Update sorting
1439
		if(is_array($siblingIDs)) {
1440
			$counter = 0;
1441
			foreach($siblingIDs as $id) {
1442
				if($id == $node->ID) {
1443
					$node->Sort = ++$counter;
0 ignored issues
show
Documentation introduced by
The property Sort does not exist on object<SilverStripe\ORM\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...
1444
					$node->write();
1445
					$statusUpdates['modified'][$node->ID] = array(
1446
						'TreeTitle' => $node->TreeTitle
1447
					);
1448
				} else if(is_numeric($id)) {
1449
					// Nodes that weren't "actually moved" shouldn't be registered as
1450
					// having been edited; do a direct SQL update instead
1451
					++$counter;
1452
					$table = DataObject::getSchema()->baseDataTable($className);
1453
					DB::prepared_query(
1454
						"UPDATE \"$table\" SET \"Sort\" = ? WHERE \"ID\" = ?",
1455
						array($counter, $id)
1456
					);
1457
				}
1458
			}
1459
1460
			$this->getResponse()->addHeader('X-Status',
1461
				rawurlencode(_t('LeftAndMain.REORGANISATIONSUCCESSFUL', 'Reorganised the site tree successfully.')));
1462
		}
1463
1464
		return Convert::raw2json($statusUpdates);
1465
	}
1466
1467
	public function CanOrganiseSitetree() {
1468
		return !Permission::check('SITETREE_REORGANISE') && !Permission::check('ADMIN') ? false : true;
1469
	}
1470
1471
	/**
1472
	 * Retrieves an edit form, either for display, or to process submitted data.
1473
	 * Also used in the template rendered through {@link Right()} in the $EditForm placeholder.
1474
	 *
1475
	 * This is a "pseudo-abstract" methoed, usually connected to a {@link getEditForm()}
1476
	 * method in an entwine subclass. This method can accept a record identifier,
1477
	 * selected either in custom logic, or through {@link currentPageID()}.
1478
	 * The form usually construct itself from {@link DataObject->getCMSFields()}
1479
	 * for the specific managed subclass defined in {@link LeftAndMain::$tree_class}.
1480
	 *
1481
	 * @param HTTPRequest $request Optionally contains an identifier for the
1482
	 *  record to load into the form.
1483
	 * @return Form Should return a form regardless wether a record has been found.
1484
	 *  Form might be readonly if the current user doesn't have the permission to edit
1485
	 *  the record.
1486
	 */
1487
	/**
1488
	 * @return Form
1489
	 */
1490
	public function EditForm($request = null) {
1491
		return $this->getEditForm();
1492
	}
1493
1494
	/**
1495
	 * Calls {@link SiteTree->getCMSFields()}
1496
	 *
1497
	 * @param Int $id
1498
	 * @param FieldList $fields
1499
	 * @return Form
1500
	 */
1501
	public function getEditForm($id = null, $fields = null) {
1502
		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...
1503
1504
		if(is_object($id)) {
1505
			$record = $id;
1506
		} else {
1507
			$record = $this->getRecord($id);
1508
			if($record && !$record->canView()) return Security::permissionFailure($this);
1509
		}
1510
1511
		if($record) {
1512
			$fields = ($fields) ? $fields : $record->getCMSFields();
1513
			if ($fields == null) {
1514
				user_error(
1515
					"getCMSFields() returned null  - it should return a FieldList object.
1516
					Perhaps you forgot to put a return statement at the end of your method?",
1517
					E_USER_ERROR
1518
				);
1519
			}
1520
1521
			// Add hidden fields which are required for saving the record
1522
			// and loading the UI state
1523
			if(!$fields->dataFieldByName('ClassName')) {
1524
				$fields->push(new HiddenField('ClassName'));
1525
			}
1526
1527
			$tree_class = $this->stat('tree_class');
1528
			if(
1529
				$tree_class::has_extension(Hierarchy::class)
1530
				&& !$fields->dataFieldByName('ParentID')
1531
			) {
1532
				$fields->push(new HiddenField('ParentID'));
1533
			}
1534
1535
			// Added in-line to the form, but plucked into different view by frontend scripts.
1536
			if ($record instanceof CMSPreviewable) {
1537
				/** @skipUpgrade */
1538
				$navField = new LiteralField('SilverStripeNavigator', $this->getSilverStripeNavigator());
1539
				$navField->setAllowHTML(true);
1540
				$fields->push($navField);
1541
			}
1542
1543
			if($record->hasMethod('getAllCMSActions')) {
1544
				$actions = $record->getAllCMSActions();
1545
			} else {
1546
				$actions = $record->getCMSActions();
1547
				// add default actions if none are defined
1548
				if(!$actions || !$actions->count()) {
1549
					if($record->hasMethod('canEdit') && $record->canEdit()) {
1550
						$actions->push(
1551
							FormAction::create('save',_t('CMSMain.SAVE','Save'))
1552
								->addExtraClass('ss-ui-action-constructive')->setAttribute('data-icon', 'accept')
1553
						);
1554
					}
1555
					if($record->hasMethod('canDelete') && $record->canDelete()) {
1556
						$actions->push(
1557
							FormAction::create('delete',_t('ModelAdmin.DELETE','Delete'))
1558
								->addExtraClass('ss-ui-action-destructive')
1559
						);
1560
					}
1561
				}
1562
			}
1563
1564
			// Use <button> to allow full jQuery UI styling
1565
			$actionsFlattened = $actions->dataFields();
1566
			if($actionsFlattened) {
1567
				/** @var FormAction $action */
1568
				foreach($actionsFlattened as $action) {
1569
					$action->setUseButtonTag(true);
1570
				}
1571
			}
1572
1573
			$negotiator = $this->getResponseNegotiator();
1574
			$form = Form::create(
1575
				$this, "EditForm", $fields, $actions
1576
			)->setHTMLID('Form_EditForm');
1577
			$form->addExtraClass('cms-edit-form');
1578
			$form->loadDataFrom($record);
1579
			$form->setTemplate($this->getTemplatesWithSuffix('_EditForm'));
1580
			$form->setAttribute('data-pjax-fragment', 'CurrentForm');
1581
			$form->setValidationResponseCallback(function(ValidationResult $errors) use ($negotiator, $form) {
0 ignored issues
show
Unused Code introduced by
The parameter $errors 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...
1582
				$request = $this->getRequest();
1583
				if($request->isAjax() && $negotiator) {
1584
					$result = $form->forTemplate();
1585
1586
					return $negotiator->respond($request, array(
1587
						'CurrentForm' => function() use($result) {
1588
							return $result;
1589
						}
1590
					));
1591
				}
1592
			});
1593
1594
			// Announce the capability so the frontend can decide whether to allow preview or not.
1595
			if ($record instanceof CMSPreviewable) {
1596
				$form->addExtraClass('cms-previewable');
1597
			}
1598
			$form->addExtraClass('fill-height');
1599
1600
			// Set this if you want to split up tabs into a separate header row
1601
			// 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...
1602
			// 	$form->Fields()->findOrMakeTab('Root')->setTemplate('SilverStripe\\Forms\\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...
1603
			// }
1604
1605
			// Add a default or custom validator.
1606
			// @todo Currently the default Validator.js implementation
1607
			//  adds javascript to the document body, meaning it won't
1608
			//  be included properly if the associated fields are loaded
1609
			//  through ajax. This means only serverside validation
1610
			//  will kick in for pages+validation loaded through ajax.
1611
			//  This will be solved by using less obtrusive javascript validation
1612
			//  in the future, see http://open.silverstripe.com/ticket/2915 and
1613
			//  http://open.silverstripe.com/ticket/3386
1614
			if($record->hasMethod('getCMSValidator')) {
1615
				$validator = $record->getCMSValidator();
1616
				// The clientside (mainly LeftAndMain*.js) rely on ajax responses
1617
				// which can be evaluated as javascript, hence we need
1618
				// to override any global changes to the validation handler.
1619
				if($validator != NULL){
1620
					$form->setValidator($validator);
1621
				}
1622
			} else {
1623
				$form->unsetValidator();
1624
			}
1625
1626
			if($record->hasMethod('canEdit') && !$record->canEdit()) {
1627
				$readonlyFields = $form->Fields()->makeReadonly();
1628
				$form->setFields($readonlyFields);
1629
			}
1630
		} else {
1631
			$form = $this->EmptyForm();
1632
		}
1633
1634
		return $form;
1635
	}
1636
1637
	/**
1638
	 * Returns a placeholder form, used by {@link getEditForm()} if no record is selected.
1639
	 * Our javascript logic always requires a form to be present in the CMS interface.
1640
	 *
1641
	 * @return Form
1642
	 */
1643
	public function EmptyForm() {
1644
		$form = Form::create(
1645
			$this,
1646
			"EditForm",
1647
			new FieldList(
1648
				// new HeaderField(
1649
				// 	'WelcomeHeader',
1650
				// 	$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...
1651
				// ),
1652
				// new LiteralField(
1653
				// 	'WelcomeText',
1654
				// 	sprintf('<p id="WelcomeMessage">%s %s. %s</p>',
1655
				// 		_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...
1656
				// 		$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...
1657
				// 		_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...
1658
				// 	)
1659
				// )
1660
			),
1661
			new FieldList()
1662
		)->setHTMLID('Form_EditForm');
1663
		$form->unsetValidator();
1664
		$form->addExtraClass('cms-edit-form');
1665
		$form->addExtraClass('root-form');
1666
		$form->setTemplate($this->getTemplatesWithSuffix('_EditForm'));
1667
		$form->setAttribute('data-pjax-fragment', 'CurrentForm');
1668
1669
		return $form;
1670
	}
1671
1672
	/**
1673
	 * Return the CMS's HTML-editor toolbar
1674
	 */
1675
	public function EditorToolbar() {
1676
		return HTMLEditorField_Toolbar::create($this, "EditorToolbar");
1677
	}
1678
1679
	/**
1680
	 * Renders a panel containing tools which apply to all displayed
1681
	 * "content" (mostly through {@link EditForm()}), for example a tree navigation or a filter panel.
1682
	 * Auto-detects applicable templates by naming convention: "<controller classname>_Tools.ss",
1683
	 * and takes the most specific template (see {@link getTemplatesWithSuffix()}).
1684
	 * To explicitly disable the panel in the subclass, simply create a more specific, empty template.
1685
	 *
1686
	 * @return String HTML
1687
	 */
1688
	public function Tools() {
1689
		$templates = $this->getTemplatesWithSuffix('_Tools');
1690
		if($templates) {
1691
			$viewer = new SSViewer($templates);
1692
			return $viewer->process($this);
1693
		} else {
1694
			return false;
1695
		}
1696
	}
1697
1698
	/**
1699
	 * Renders a panel containing tools which apply to the currently displayed edit form.
1700
	 * The main difference to {@link Tools()} is that the panel is displayed within
1701
	 * the element structure of the form panel (rendered through {@link EditForm}).
1702
	 * This means the panel will be loaded alongside new forms, and refreshed upon save,
1703
	 * which can mean a performance hit, depending on how complex your panel logic gets.
1704
	 * Any form fields contained in the returned markup will also be submitted with the main form,
1705
	 * which might be desired depending on the implementation details.
1706
	 *
1707
	 * @return String HTML
1708
	 */
1709
	public function EditFormTools() {
1710
		$templates = $this->getTemplatesWithSuffix('_EditFormTools');
1711
		if($templates) {
1712
			$viewer = new SSViewer($templates);
1713
			return $viewer->process($this);
1714
		} else {
1715
			return false;
1716
		}
1717
	}
1718
1719
	/**
1720
	 * Batch Actions Handler
1721
	 */
1722
	public function batchactions() {
1723
		return new CMSBatchActionHandler($this, 'batchactions', $this->stat('tree_class'));
1724
	}
1725
1726
	/**
1727
	 * @return Form
1728
	 */
1729
	public function BatchActionsForm() {
1730
		$actions = $this->batchactions()->batchActionList();
1731
		$actionsMap = array('-1' => _t('LeftAndMain.DropdownBatchActionsDefault', 'Choose an action...')); // Placeholder action
1732
		foreach($actions as $action) {
1733
			$actionsMap[$action->Link] = $action->Title;
1734
		}
1735
1736
		$form = new Form(
1737
			$this,
1738
			'BatchActionsForm',
1739
			new FieldList(
1740
				new HiddenField('csvIDs'),
1741
				DropdownField::create(
1742
					'Action',
1743
					false,
1744
					$actionsMap
1745
				)
1746
					->setAttribute('autocomplete', 'off')
1747
					->setAttribute('data-placeholder', _t('LeftAndMain.DropdownBatchActionsDefault', 'Choose an action...'))
1748
			),
1749
			new FieldList(
1750
				// TODO i18n
1751
				new FormAction('submit', _t('Form.SubmitBtnLabel', "Go"))
1752
			)
1753
		);
1754
		$form->addExtraClass('cms-batch-actions form--no-dividers');
1755
		$form->unsetValidator();
1756
1757
		$this->extend('updateBatchActionsForm', $form);
1758
		return $form;
1759
	}
1760
1761
	public function printable() {
1762
		$form = $this->getEditForm($this->currentPageID());
1763
		if(!$form) return false;
1764
1765
		$form->transform(new PrintableTransformation());
1766
		$form->setActions(null);
1767
1768
		Requirements::clear();
1769
		Requirements::css(FRAMEWORK_ADMIN_DIR . '/dist/css/LeftAndMain_printable.css');
1770
		return array(
1771
			"PrintForm" => $form
1772
		);
1773
	}
1774
1775
	/**
1776
	 * Used for preview controls, mainly links which switch between different states of the page.
1777
	 *
1778
	 * @return DBHTMLText
1779
	 */
1780
	public function getSilverStripeNavigator() {
1781
		$page = $this->currentPage();
1782
		if ($page instanceof CMSPreviewable) {
1783
			$navigator = new SilverStripeNavigator($page);
1784
			return $navigator->renderWith($this->getTemplatesWithSuffix('_SilverStripeNavigator'));
1785
		}
1786
		return null;
1787
	}
1788
1789
	/**
1790
	 * Identifier for the currently shown record,
1791
	 * in most cases a database ID. Inspects the following
1792
	 * sources (in this order):
1793
	 * - GET/POST parameter named 'ID'
1794
	 * - URL parameter named 'ID'
1795
	 * - Session value namespaced by classname, e.g. "CMSMain.currentPage"
1796
	 *
1797
	 * @return int
1798
	 */
1799
	public function currentPageID() {
1800
		if($this->getRequest()->requestVar('ID') && is_numeric($this->getRequest()->requestVar('ID')))	{
1801
			return $this->getRequest()->requestVar('ID');
1802
		} elseif ($this->getRequest()->requestVar('CMSMainCurrentPageID') && is_numeric($this->getRequest()->requestVar('CMSMainCurrentPageID'))) {
1803
			// see GridFieldDetailForm::ItemEditForm
1804
			return $this->getRequest()->requestVar('CMSMainCurrentPageID');
1805
		} elseif (isset($this->urlParams['ID']) && is_numeric($this->urlParams['ID'])) {
1806
			return $this->urlParams['ID'];
1807
		} elseif(Session::get($this->sessionNamespace() . ".currentPage")) {
1808
			return Session::get($this->sessionNamespace() . ".currentPage");
1809
		} else {
1810
			return null;
1811
		}
1812
	}
1813
1814
	/**
1815
	 * Forces the current page to be set in session,
1816
	 * which can be retrieved later through {@link currentPageID()}.
1817
	 * Keep in mind that setting an ID through GET/POST or
1818
	 * as a URL parameter will overrule this value.
1819
	 *
1820
	 * @param int $id
1821
	 */
1822
	public function setCurrentPageID($id) {
1823
		$id = (int)$id;
1824
		Session::set($this->sessionNamespace() . ".currentPage", $id);
1825
	}
1826
1827
	/**
1828
	 * Uses {@link getRecord()} and {@link currentPageID()}
1829
	 * to get the currently selected record.
1830
	 *
1831
	 * @return DataObject
1832
	 */
1833
	public function currentPage() {
1834
		return $this->getRecord($this->currentPageID());
1835
	}
1836
1837
	/**
1838
	 * Compares a given record to the currently selected one (if any).
1839
	 * Used for marking the current tree node.
1840
	 *
1841
	 * @param DataObject $record
1842
	 * @return bool
1843
	 */
1844
	public function isCurrentPage(DataObject $record) {
1845
		return ($record->ID == $this->currentPageID());
1846
	}
1847
1848
	/**
1849
	 * @return String
1850
	 */
1851
	protected function sessionNamespace() {
1852
		$override = $this->stat('session_namespace');
1853
		return $override ? $override : $this->class;
1854
	}
1855
1856
	/**
1857
	 * URL to a previewable record which is shown through this controller.
1858
	 * The controller might not have any previewable content, in which case
1859
	 * this method returns FALSE.
1860
	 *
1861
	 * @return String|boolean
1862
	 */
1863
	public function LinkPreview() {
1864
		return false;
1865
	}
1866
1867
	/**
1868
	 * Return the version number of this application.
1869
	 * Uses the number in <mymodule>/silverstripe_version
1870
	 * (automatically replaced by build scripts).
1871
	 * If silverstripe_version is empty,
1872
	 * then attempts to get it from composer.lock
1873
	 *
1874
	 * @return string
1875
	 */
1876
	public function CMSVersion() {
1877
		$versions = array();
1878
		$modules = array(
1879
			'silverstripe/framework' => array(
1880
				'title' => 'Framework',
1881
				'versionFile' => FRAMEWORK_PATH . '/silverstripe_version',
1882
			)
1883
		);
1884
		if(defined('CMS_PATH')) {
1885
			$modules['silverstripe/cms'] = array(
1886
				'title' => 'CMS',
1887
				'versionFile' => CMS_PATH . '/silverstripe_version',
1888
			);
1889
		}
1890
1891
		// Tries to obtain version number from composer.lock if it exists
1892
		$composerLockPath = BASE_PATH . '/composer.lock';
1893
		if (file_exists($composerLockPath)) {
1894
			$cache = Cache::factory('LeftAndMain_CMSVersion');
1895
			$cacheKey = filemtime($composerLockPath);
1896
			$versions = $cache->load($cacheKey);
1897
			if($versions) {
1898
				$versions = json_decode($versions, true);
1899
			} else {
1900
				$versions = array();
1901
			}
1902
			if(!$versions && $jsonData = file_get_contents($composerLockPath)) {
1903
				$lockData = json_decode($jsonData);
1904
				if($lockData && isset($lockData->packages)) {
1905
					foreach ($lockData->packages as $package) {
1906
						if(
1907
							array_key_exists($package->name, $modules)
1908
							&& isset($package->version)
1909
						) {
1910
							$versions[$package->name] = $package->version;
1911
						}
1912
					}
1913
					$cache->save(json_encode($versions), $cacheKey);
1914
				}
1915
			}
1916
		}
1917
1918
		// Fall back to static version file
1919
		foreach($modules as $moduleName => $moduleSpec) {
1920
			if(!isset($versions[$moduleName])) {
1921
				if($staticVersion = file_get_contents($moduleSpec['versionFile'])) {
1922
					$versions[$moduleName] = $staticVersion;
1923
				} else {
1924
					$versions[$moduleName] = _t('LeftAndMain.VersionUnknown', 'Unknown');
1925
				}
1926
			}
1927
		}
1928
1929
		$out = array();
1930
		foreach($modules as $moduleName => $moduleSpec) {
1931
			$out[] = $modules[$moduleName]['title'] . ': ' . $versions[$moduleName];
1932
		}
1933
		return implode(', ', $out);
1934
	}
1935
1936
	/**
1937
	 * @return array
1938
	 */
1939
	public function SwitchView() {
1940
		if($page = $this->currentPage()) {
1941
			$nav = SilverStripeNavigator::get_for_record($page);
1942
			return $nav['items'];
1943
		}
1944
	}
1945
1946
	/**
1947
	 * @return SiteConfig
1948
	 */
1949
	public function SiteConfig() {
1950
		return (class_exists('SilverStripe\\SiteConfig\\SiteConfig')) ? SiteConfig::current_site_config() : null;
1951
	}
1952
1953
	/**
1954
	 * The href for the anchor on the Silverstripe logo.
1955
	 * Set by calling LeftAndMain::set_application_link()
1956
	 *
1957
	 * @config
1958
	 * @var String
1959
	 */
1960
	private static $application_link = '//www.silverstripe.org/';
1961
1962
	/**
1963
	 * @return String
1964
	 */
1965
	public function ApplicationLink() {
1966
		return $this->stat('application_link');
1967
	}
1968
1969
	/**
1970
	 * The application name. Customisable by calling
1971
	 * LeftAndMain::setApplicationName() - the first parameter.
1972
	 *
1973
	 * @config
1974
	 * @var String
1975
	 */
1976
	private static $application_name = 'SilverStripe';
1977
1978
	/**
1979
	 * Get the application name.
1980
	 *
1981
	 * @return string
1982
	 */
1983
	public function getApplicationName() {
1984
		return $this->stat('application_name');
1985
	}
1986
1987
	/**
1988
	 * @return string
1989
	 */
1990
	public function Title() {
1991
		$app = $this->getApplicationName();
1992
1993
		return ($section = $this->SectionTitle()) ? sprintf('%s - %s', $app, $section) : $app;
1994
	}
1995
1996
	/**
1997
	 * Return the title of the current section. Either this is pulled from
1998
	 * the current panel's menu_title or from the first active menu
1999
	 *
2000
	 * @return string
2001
	 */
2002
	public function SectionTitle() {
2003
		$title = $this->menu_title();
2004
		if($title) {
2005
			return $title;
2006
		}
2007
2008
		foreach($this->MainMenu() as $menuItem) {
2009
			if($menuItem->LinkingMode != 'link') {
2010
				return $menuItem->Title;
2011
			}
2012
		}
2013
	}
2014
2015
	/**
2016
	 * Same as {@link ViewableData->CSSClasses()}, but with a changed name
2017
	 * to avoid problems when using {@link ViewableData->customise()}
2018
	 * (which always returns "ArrayData" from the $original object).
2019
	 *
2020
	 * @return String
2021
	 */
2022
	public function BaseCSSClasses() {
2023
		return $this->CSSClasses('SilverStripe\\Control\\Controller');
2024
	}
2025
2026
	/**
2027
	 * @return String
2028
	 */
2029
	public function Locale() {
2030
		return DBField::create_field('Locale', i18n::get_locale());
2031
	}
2032
2033
	public function providePermissions() {
2034
		$perms = array(
2035
			"CMS_ACCESS_LeftAndMain" => array(
2036
				'name' => _t('CMSMain.ACCESSALLINTERFACES', 'Access to all CMS sections'),
2037
				'category' => _t('Permission.CMS_ACCESS_CATEGORY', 'CMS Access'),
2038
				'help' => _t('CMSMain.ACCESSALLINTERFACESHELP', 'Overrules more specific access settings.'),
2039
				'sort' => -100
2040
			)
2041
		);
2042
2043
		// Add any custom ModelAdmin subclasses. Can't put this on ModelAdmin itself
2044
		// since its marked abstract, and needs to be singleton instanciated.
2045
		foreach(ClassInfo::subclassesFor('SilverStripe\\Admin\\ModelAdmin') as $i => $class) {
2046
			if ($class == 'SilverStripe\\Admin\\ModelAdmin') {
2047
				continue;
2048
			}
2049
			if (ClassInfo::classImplements($class, 'SilverStripe\\Dev\\TestOnly')) {
2050
				continue;
2051
			}
2052
2053
			// Check if modeladmin has explicit required_permission_codes option.
2054
			// If a modeladmin is namespaced you can apply this config to override
2055
			// the default permission generation based on fully qualified class name.
2056
			$code = $this->getRequiredPermissions();
2057
			if (!$code) {
2058
				continue;
2059
			}
2060
			// Get first permission if multiple specified
2061
			if (is_array($code)) {
2062
				$code = reset($code);
2063
			}
2064
			$title = LeftAndMain::menu_title($class);
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

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

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

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

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

Loading history...
2065
			$perms[$code] = array(
2066
				'name' => _t(
2067
					'CMSMain.ACCESS',
2068
					"Access to '{title}' section",
2069
					"Item in permission selection identifying the admin section. Example: Access to 'Files & Images'",
2070
					array('title' => $title)
2071
				),
2072
				'category' => _t('Permission.CMS_ACCESS_CATEGORY', 'CMS Access')
2073
			);
2074
		}
2075
2076
		return $perms;
2077
	}
2078
2079
}
2080