Completed
Pull Request — master (#6389)
by Damian
08:59
created

LeftAndMain::PreviewPanel()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 4
nc 2
nop 0
dl 0
loc 8
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
    /**
70
     * Form schema header identifier
71
     */
72
    const SCHEMA_HEADER = 'X-Formschema-Request';
73
74
    /**
75
     * Enable front-end debugging (increases verbosity) in dev mode.
76
     * Will be ignored in live environments.
77
     *
78
     * @var bool
79
     */
80
    private static $client_debugging = true;
81
82
    /**
83
     * The current url segment attached to the LeftAndMain instance
84
     *
85
     * @config
86
     * @var string
87
     */
88
    private static $url_segment;
89
90
    /**
91
     * @config
92
     * @var string
93
     */
94
    private static $url_rule = '/$Action/$ID/$OtherID';
95
96
    /**
97
     * @config
98
     * @var string
99
     */
100
    private static $menu_title;
101
102
    /**
103
     * @config
104
     * @var string
105
     */
106
    private static $menu_icon;
107
108
    /**
109
     * @config
110
     * @var int
111
     */
112
    private static $menu_priority = 0;
113
114
    /**
115
     * @config
116
     * @var int
117
     */
118
    private static $url_priority = 50;
119
120
    /**
121
     * A subclass of {@link DataObject}.
122
     *
123
     * Determines what is managed in this interface, through
124
     * {@link getEditForm()} and other logic.
125
     *
126
     * @config
127
     * @var string
128
     */
129
    private static $tree_class = null;
130
131
    /**
132
     * The url used for the link in the Help tab in the backend
133
     *
134
     * @config
135
     * @var string
136
     */
137
    private static $help_link = '//userhelp.silverstripe.org/framework/en/3.3';
138
139
    /**
140
     * @var array
141
     */
142
    private static $allowed_actions = [
143
        'index',
144
        'save',
145
        'savetreenode',
146
        'getsubtree',
147
        'updatetreenodes',
148
        'printable',
149
        'show',
150
        'EditorToolbar',
151
        'EditForm',
152
        'AddForm',
153
        'batchactions',
154
        'BatchActionsForm',
155
        'schema',
156
    ];
157
158
    private static $url_handlers = [
159
        'GET schema/$FormName/$ItemID/$OtherItemID' => 'schema'
160
    ];
161
162
    private static $dependencies = [
163
        'FormSchema' => '%$FormSchema'
164
    ];
165
166
    /**
167
     * Current form schema helper
168
     *
169
     * @var FormSchema
170
     */
171
    protected $schema = null;
172
173
    /**
174
     * Assign themes to use for cms
175
     *
176
     * @config
177
     * @var array
178
     */
179
    private static $admin_themes = [
180
        'silverstripe/framework:/admin/themes/cms-forms',
181
        SSViewer::DEFAULT_THEME,
182
    ];
183
184
    /**
185
     * Codes which are required from the current user to view this controller.
186
     * If multiple codes are provided, all of them are required.
187
     * All CMS controllers require "CMS_ACCESS_LeftAndMain" as a baseline check,
188
     * and fall back to "CMS_ACCESS_<class>" if no permissions are defined here.
189
     * See {@link canView()} for more details on permission checks.
190
     *
191
     * @config
192
     * @var array
193
     */
194
    private static $required_permission_codes;
195
196
    /**
197
     * @config
198
     * @var String Namespace for session info, e.g. current record.
199
     * Defaults to the current class name, but can be amended to share a namespace in case
200
     * controllers are logically bundled together, and mainly separated
201
     * to achieve more flexible templating.
202
     */
203
    private static $session_namespace;
204
205
    /**
206
     * Register additional requirements through the {@link Requirements} class.
207
     * Used mainly to work around the missing "lazy loading" functionality
208
     * for getting css/javascript required after an ajax-call (e.g. loading the editform).
209
     *
210
     * YAML configuration example:
211
     * <code>
212
     * LeftAndMain:
213
     *   extra_requirements_javascript:
214
     *     - mysite/javascript/myscript.js
215
     * </code>
216
     *
217
     * @config
218
     * @var array
219
     */
220
    private static $extra_requirements_javascript = array();
221
222
    /**
223
     * YAML configuration example:
224
     * <code>
225
     * LeftAndMain:
226
     *   extra_requirements_css:
227
     *     - mysite/css/mystyle.css:
228
    *          media: screen
229
     * </code>
230
     *
231
     * @config
232
     * @var array See {@link extra_requirements_javascript}
233
     */
234
    private static $extra_requirements_css = array();
235
236
    /**
237
     * @config
238
     * @var array See {@link extra_requirements_javascript}
239
     */
240
    private static $extra_requirements_themedCss = array();
241
242
    /**
243
     * If true, call a keepalive ping every 5 minutes from the CMS interface,
244
     * to ensure that the session never dies.
245
     *
246
     * @config
247
     * @var boolean
248
     */
249
    private static $session_keepalive_ping = true;
250
251
    /**
252
     * Value of X-Frame-Options header
253
     *
254
     * @config
255
     * @var string
256
     */
257
    private static $frame_options = 'SAMEORIGIN';
258
259
    /**
260
     * @var PjaxResponseNegotiator
261
     */
262
    protected $responseNegotiator;
263
264
    /**
265
     * Gets the combined configuration of all LeafAndMain subclasses required by the client app.
266
     *
267
     * @return array
268
     *
269
     * WARNING: Experimental API
270
     */
271
    public function getCombinedClientConfig()
272
    {
273
        $combinedClientConfig = ['sections' => []];
274
        $cmsClassNames = CMSMenu::get_cms_classes('SilverStripe\\Admin\\LeftAndMain', true, CMSMenu::URL_PRIORITY);
275
276
        foreach ($cmsClassNames as $className) {
277
            $combinedClientConfig['sections'][$className] =  Injector::inst()->get($className)->getClientConfig();
278
        }
279
280
        // Pass in base url (absolute and relative)
281
        $combinedClientConfig['baseUrl'] = Director::baseURL();
282
        $combinedClientConfig['absoluteBaseUrl'] = Director::absoluteBaseURL();
283
        $combinedClientConfig['adminUrl'] = AdminRootController::admin_url();
284
285
        // Get "global" CSRF token for use in JavaScript
286
        $token = SecurityToken::inst();
287
        $combinedClientConfig[$token->getName()] = $token->getValue();
288
289
        // Set env
290
        $combinedClientConfig['environment'] = Director::get_environment_type();
291
        $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...
292
293
        return Convert::raw2json($combinedClientConfig);
294
    }
295
296
    /**
297
     * Returns configuration required by the client app.
298
     *
299
     * @return array
300
     *
301
     * WARNING: Experimental API
302
     */
303
    public function getClientConfig()
304
    {
305
        return [
306
            // Trim leading/trailing slash to make it easier to concatenate URL
307
            // and use in routing definitions.
308
            'url' => trim($this->Link(), '/'),
309
        ];
310
    }
311
312
    /**
313
     * Get form schema helper
314
     *
315
     * @return FormSchema
316
     */
317
    public function getFormSchema()
318
    {
319
        return $this->schema;
320
    }
321
322
    /**
323
     * Set form schema helper for this controller
324
     *
325
     * @param FormSchema $schema
326
     * @return $this
327
     */
328
    public function setFormSchema(FormSchema $schema)
329
    {
330
        $this->schema = $schema;
331
        return $this;
332
    }
333
334
    /**
335
     * Gets a JSON schema representing the current edit form.
336
     *
337
     * WARNING: Experimental API.
338
     *
339
     * @param HTTPRequest $request
340
     * @return HTTPResponse
341
     */
342
    public function schema($request)
343
    {
344
        $formName = $request->param('FormName');
345
        $itemID = $request->param('ItemID');
346
347
        if (!$formName) {
348
            return (new HTTPResponse('Missing request params', 400));
349
        }
350
    
351
        $formMethod = "get{$formName}";
352
        if (!$this->hasMethod($formMethod)) {
353
            return (new HTTPResponse('Form not found', 404));
354
        }
355
356
        if (!$this->hasAction($formName)) {
357
            return (new HTTPResponse('Form not accessible', 401));
358
        }
359
    
360
        if ($itemID) {
361
            $form = $this->{$formMethod}($itemID);
362
        } else {
363
            $form = $this->{$formMethod}();
364
        }
365
        $schemaID = $request->getURL();
366
        return $this->getSchemaResponse($schemaID, $form);
367
    }
368
369
    /**
370
     * Check if the current request has a X-Formschema-Request header set.
371
     * Used by conditional logic that responds to validation results
372
     *
373
     * @return bool
374
     */
375
    protected function getSchemaRequested()
376
    {
377
        $parts = $this->getRequest()->getHeader(static::SCHEMA_HEADER);
378
        return !empty($parts);
379
    }
380
381
    /**
382
     * Generate schema for the given form based on the X-Formschema-Request header value
383
     *
384
     * @param string $schemaID ID for this schema. Required.
385
     * @param Form $form Required for 'state' or 'schema' response
386
     * @param ValidationResult $errors Required for 'error' response
387
     * @param array $extraData Any extra data to be merged with the schema response
388
     * @return HTTPResponse
389
     */
390
    protected function getSchemaResponse($schemaID, $form = null, ValidationResult $errors = null, $extraData = [])
391
    {
392
        $parts = $this->getRequest()->getHeader(static::SCHEMA_HEADER);
393
        $data = $this
394
            ->getFormSchema()
395
            ->getMultipartSchema($parts, $schemaID, $form, $errors);
396
397
        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...
398
            $data = array_merge($data, $extraData);
399
        }
400
401
        $response = new HTTPResponse(Convert::raw2json($data));
402
        $response->addHeader('Content-Type', 'application/json');
403
        return $response;
404
    }
405
406
    /**
407
     * Get link to schema url for a given form
408
     *
409
     * @param Form $form
410
     * @return string
411
     */
412
    protected function getSchemaLinkForForm(Form $form)
413
    {
414
        $parts = [$this->Link('schema'), $form->getName()];
415
        if (($record = $form->getRecord()) && $record->isInDB()) {
416
            $parts[] = $record->ID;
417
        } elseif (($data = $form->getData()) && !empty($data['ID'])) {
418
            $parts[] = $data['ID'];
419
        }
420
        return Controller::join_links($parts);
421
    }
422
423
    /**
424
     * @param Member $member
425
     * @return boolean
426
     */
427
    public function canView($member = null)
428
    {
429
        if (!$member && $member !== false) {
430
            $member = Member::currentUser();
431
        }
432
433
        // cms menus only for logged-in members
434
        if (!$member) {
435
            return false;
436
        }
437
438
        // alternative extended checks
439
        if ($this->hasMethod('alternateAccessCheck')) {
440
            $alternateAllowed = $this->alternateAccessCheck();
441
            if ($alternateAllowed === false) {
442
                return false;
443
            }
444
        }
445
446
        // Check for "CMS admin" permission
447
        if (Permission::checkMember($member, "CMS_ACCESS_LeftAndMain")) {
448
            return true;
449
        }
450
451
        // Check for LeftAndMain sub-class permissions
452
        $codes = $this->getRequiredPermissions();
453
        if ($codes === false) { // allow explicit FALSE to disable subclass check
454
            return true;
455
        }
456
        foreach ((array)$codes as $code) {
457
            if (!Permission::checkMember($member, $code)) {
458
                return false;
459
            }
460
        }
461
462
        return true;
463
    }
464
465
    /**
466
     * Get list of required permissions
467
     *
468
     * @return array|string|bool Code, array of codes, or false if no permission required
469
     */
470
    public static function getRequiredPermissions()
471
    {
472
        $class = get_called_class();
473
        $code = Config::inst()->get($class, 'required_permission_codes', Config::FIRST_SET);
474
        if ($code === false) {
475
            return false;
476
        }
477
        if ($code) {
478
            return $code;
479
        }
480
        return "CMS_ACCESS_" . $class;
481
    }
482
483
    /**
484
     * @uses LeftAndMainExtension->init()
485
     * @uses LeftAndMainExtension->accessedCMS()
486
     * @uses CMSMenu
487
     */
488
    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...
489
    {
490
        parent::init();
491
492
        SSViewer::config()->update('rewrite_hash_links', false);
493
        ContentNegotiator::config()->update('enabled', false);
494
495
        // set language
496
        $member = Member::currentUser();
497
        if (!empty($member->Locale)) {
498
            i18n::set_locale($member->Locale);
499
        }
500
        if (!empty($member->DateFormat)) {
501
            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...
502
        }
503
        if (!empty($member->TimeFormat)) {
504
            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...
505
        }
506
507
        // can't be done in cms/_config.php as locale is not set yet
508
        CMSMenu::add_link(
509
            'Help',
510
            _t('LeftAndMain.HELP', 'Help', 'Menu title'),
511
            $this->config()->help_link,
512
            -2,
513
            array(
514
                'target' => '_blank'
515
            )
516
        );
517
518
        // Allow customisation of the access check by a extension
519
        // Also all the canView() check to execute Controller::redirect()
520
        if (!$this->canView() && !$this->getResponse()->isFinished()) {
521
            // When access /admin/, we should try a redirect to another part of the admin rather than be locked out
522
            $menu = $this->MainMenu();
523
            foreach ($menu as $candidate) {
524
                if ($candidate->Link &&
525
                    $candidate->Link != $this->Link()
526
                    && $candidate->MenuItem->controller
527
                    && singleton($candidate->MenuItem->controller)->canView()
528
                ) {
529
                    $this->redirect($candidate->Link);
530
                    return;
531
                }
532
            }
533
534
            if (Member::currentUser()) {
535
                Session::set("BackURL", null);
536
            }
537
538
            // if no alternate menu items have matched, return a permission error
539
            $messageSet = array(
540
                'default' => _t(
541
                    'LeftAndMain.PERMDEFAULT',
542
                    "You must be logged in to access the administration area; please enter your credentials below."
543
                ),
544
                'alreadyLoggedIn' => _t(
545
                    'LeftAndMain.PERMALREADY',
546
                    "I'm sorry, but you can't access that part of the CMS.  If you want to log in as someone else, do"
547
                    . " so below."
548
                ),
549
                'logInAgain' => _t(
550
                    'LeftAndMain.PERMAGAIN',
551
                    "You have been logged out of the CMS.  If you would like to log in again, enter a username and"
552
                    . " password below."
553
                ),
554
            );
555
556
            Security::permissionFailure($this, $messageSet);
557
            return;
558
        }
559
560
        // Don't continue if there's already been a redirection request.
561
        if ($this->redirectedTo()) {
562
            return;
563
        }
564
565
        // Audit logging hook
566
        if (empty($_REQUEST['executeForm']) && !$this->getRequest()->isAjax()) {
567
            $this->extend('accessedCMS');
568
        }
569
570
        // Set the members html editor config
571
        if (Member::currentUser()) {
572
            HTMLEditorConfig::set_active_identifier(Member::currentUser()->getHtmlEditorConfigForCMS());
573
        }
574
575
        // Set default values in the config if missing.  These things can't be defined in the config
576
        // file because insufficient information exists when that is being processed
577
        $htmlEditorConfig = HTMLEditorConfig::get_active();
578
        $htmlEditorConfig->setOption('language', i18n::get_tinymce_lang());
579
580
        Requirements::customScript("
581
			window.ss = window.ss || {};
582
			window.ss.config = " . $this->getCombinedClientConfig() . ";
583
		");
584
585
        Requirements::javascript(FRAMEWORK_ADMIN_DIR . '/client/dist/js/vendor.js');
586
        Requirements::javascript(FRAMEWORK_ADMIN_DIR . '/client/dist/js/bundle.js');
587
        Requirements::css(ltrim(FRAMEWORK_ADMIN_DIR . '/client/dist/styles/bundle.css', '/'));
588
589
        Requirements::add_i18n_javascript(ltrim(FRAMEWORK_DIR . '/client/lang', '/'), false, true);
590
        Requirements::add_i18n_javascript(FRAMEWORK_ADMIN_DIR . '/client/lang', false, true);
591
592
        if ($this->config()->session_keepalive_ping) {
593
            Requirements::javascript(FRAMEWORK_ADMIN_DIR . '/client/dist/js/LeftAndMain.Ping.js');
594
        }
595
596
        if (Director::isDev()) {
597
            // TODO Confuses jQuery.ondemand through document.write()
598
            Requirements::javascript(ADMIN_THIRDPARTY_DIR . '/jquery-entwine/src/jquery.entwine.inspector.js');
599
            Requirements::javascript(FRAMEWORK_ADMIN_DIR . '/client/dist/js/leaktools.js');
600
        }
601
602
        // Custom requirements
603
        $extraJs = $this->stat('extra_requirements_javascript');
604
605
        if ($extraJs) {
606
            foreach ($extraJs as $file => $config) {
607
                if (is_numeric($file)) {
608
                    $file = $config;
609
                }
610
611
                Requirements::javascript($file);
612
            }
613
        }
614
615
        $extraCss = $this->stat('extra_requirements_css');
616
617
        if ($extraCss) {
618
            foreach ($extraCss as $file => $config) {
619
                if (is_numeric($file)) {
620
                    $file = $config;
621
                    $config = array();
622
                }
623
624
                Requirements::css($file, isset($config['media']) ? $config['media'] : null);
625
            }
626
        }
627
628
        $extraThemedCss = $this->stat('extra_requirements_themedCss');
629
630
        if ($extraThemedCss) {
631
            foreach ($extraThemedCss as $file => $config) {
632
                if (is_numeric($file)) {
633
                    $file = $config;
634
                    $config = array();
635
                }
636
637
                Requirements::themedCSS($file, isset($config['media']) ? $config['media'] : null);
638
            }
639
        }
640
641
        $dummy = null;
642
        $this->extend('init', $dummy);
643
644
        // Assign default cms theme and replace user-specified themes
645
        SSViewer::set_themes($this->config()->admin_themes);
646
647
        //set the reading mode for the admin to stage
648
        Versioned::set_stage(Versioned::DRAFT);
649
    }
650
651
    public function handleRequest(HTTPRequest $request, DataModel $model = null)
652
    {
653
        try {
654
            $response = parent::handleRequest($request, $model);
0 ignored issues
show
Bug introduced by
It seems like $model defined by parameter $model on line 651 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...
655
        } catch (ValidationException $e) {
656
            // Nicer presentation of model-level validation errors
657
            $msgs = _t('LeftAndMain.ValidationError', 'Validation error') . ': '
658
                . $e->getMessage();
659
            $e = new HTTPResponse_Exception($msgs, 403);
660
            $errorResponse = $e->getResponse();
661
            $errorResponse->addHeader('Content-Type', 'text/plain');
662
            $errorResponse->addHeader('X-Status', rawurlencode($msgs));
663
            $e->setResponse($errorResponse);
664
            throw $e;
665
        }
666
667
        $title = $this->Title();
668
        if (!$response->getHeader('X-Controller')) {
669
            $response->addHeader('X-Controller', $this->class);
670
        }
671
        if (!$response->getHeader('X-Title')) {
672
            $response->addHeader('X-Title', urlencode($title));
673
        }
674
675
        // Prevent clickjacking, see https://developer.mozilla.org/en-US/docs/HTTP/X-Frame-Options
676
        $originalResponse = $this->getResponse();
677
        $originalResponse->addHeader('X-Frame-Options', $this->config()->frame_options);
678
        $originalResponse->addHeader('Vary', 'X-Requested-With');
679
680
        return $response;
681
    }
682
683
    /**
684
     * Overloaded redirection logic to trigger a fake redirect on ajax requests.
685
     * While this violates HTTP principles, its the only way to work around the
686
     * fact that browsers handle HTTP redirects opaquely, no intervention via JS is possible.
687
     * In isolation, that's not a problem - but combined with history.pushState()
688
     * it means we would request the same redirection URL twice if we want to update the URL as well.
689
     * See LeftAndMain.js for the required jQuery ajaxComplete handlers.
690
     *
691
     * @param string $url
692
     * @param int $code
693
     * @return HTTPResponse|string
694
     */
695
    public function redirect($url, $code = 302)
696
    {
697
        if ($this->getRequest()->isAjax()) {
698
            $response = $this->getResponse();
699
            $response->addHeader('X-ControllerURL', $url);
700
            if ($this->getRequest()->getHeader('X-Pjax') && !$response->getHeader('X-Pjax')) {
701
                $response->addHeader('X-Pjax', $this->getRequest()->getHeader('X-Pjax'));
702
            }
703
            $newResponse = new LeftAndMain_HTTPResponse(
704
                $response->getBody(),
705
                $response->getStatusCode(),
706
                $response->getStatusDescription()
707
            );
708
            foreach ($response->getHeaders() as $k => $v) {
709
                $newResponse->addHeader($k, $v);
710
            }
711
            $newResponse->setIsFinished(true);
712
            $this->setResponse($newResponse);
713
            return ''; // Actual response will be re-requested by client
714
        } else {
715
            parent::redirect($url, $code);
716
        }
717
    }
718
719
    /**
720
     * @param HTTPRequest $request
721
     * @return HTTPResponse
722
     */
723
    public function index($request)
724
    {
725
        return $this->getResponseNegotiator()->respond($request);
726
    }
727
728
    /**
729
     * If this is set to true, the "switchView" context in the
730
     * template is shown, with links to the staging and publish site.
731
     *
732
     * @return boolean
733
     */
734
    public function ShowSwitchView()
735
    {
736
        return false;
737
    }
738
739
740
    //------------------------------------------------------------------------------------------//
741
    // Main controllers
742
743
    /**
744
     * You should implement a Link() function in your subclass of LeftAndMain,
745
     * to point to the URL of that particular controller.
746
     *
747
     * @param string $action
748
     * @return string
749
     */
750
    public function Link($action = null)
751
    {
752
        // Handle missing url_segments
753
        if ($this->config()->url_segment) {
754
            $segment = $this->config()->get('url_segment', Config::FIRST_SET);
755
        } else {
756
            $segment = $this->class;
757
        };
758
759
        $link = Controller::join_links(
760
            AdminRootController::admin_url(),
761
            $segment,
762
            '/', // trailing slash needed if $action is null!
763
            "$action"
764
        );
765
        $this->extend('updateLink', $link);
766
        return $link;
767
    }
768
769
    /**
770
     * @deprecated 5.0
771
     */
772
    public static function menu_title_for_class($class)
773
    {
774
        Deprecation::notice('5.0', 'Use menu_title() instead');
775
        return static::menu_title($class, false);
776
    }
777
778
    /**
779
     * Get menu title for this section (translated)
780
     *
781
     * @param string $class Optional class name if called on LeftAndMain directly
782
     * @param bool $localise Determine if menu title should be localised via i18n.
783
     * @return string Menu title for the given class
784
     */
785
    public static function menu_title($class = null, $localise = true)
786
    {
787
        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...
788
            // Respect oveloading of menu_title() in subclasses
789
            return $class::menu_title(null, $localise);
790
        }
791
        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...
792
            $class = get_called_class();
793
        }
794
795
        // Get default class title
796
        $title = Config::inst()->get($class, 'menu_title', Config::FIRST_SET);
797
        if (!$title) {
798
            $title = preg_replace('/Admin$/', '', $class);
799
        }
800
801
        // Check localisation
802
        if (!$localise) {
803
            return $title;
804
        }
805
        return i18n::_t("{$class}.MENUTITLE", $title);
806
    }
807
808
    /**
809
     * Return styling for the menu icon, if a custom icon is set for this class
810
     *
811
     * Example: static $menu-icon = '/path/to/image/';
812
     * @param string $class
813
     * @return string
814
     */
815
    public static function menu_icon_for_class($class)
816
    {
817
        $icon = Config::inst()->get($class, 'menu_icon', Config::FIRST_SET);
818
        if (!empty($icon)) {
819
            $class = strtolower(Convert::raw2htmlname(str_replace('\\', '-', $class)));
820
            return ".icon.icon-16.icon-{$class} { background-image: url('{$icon}'); } ";
821
        }
822
        return '';
823
    }
824
825
    /**
826
     * @param HTTPRequest $request
827
     * @return HTTPResponse
828
     * @throws HTTPResponse_Exception
829
     */
830
    public function show($request)
831
    {
832
        // TODO Necessary for TableListField URLs to work properly
833
        if ($request->param('ID')) {
834
            $this->setCurrentPageID($request->param('ID'));
835
        }
836
        return $this->getResponseNegotiator()->respond($request);
837
    }
838
839
    /**
840
     * Caution: Volatile API.
841
     *
842
     * @return PjaxResponseNegotiator
843
     */
844
    public function getResponseNegotiator()
845
    {
846
        if (!$this->responseNegotiator) {
847
            $controller = $this;
848
            $this->responseNegotiator = new PjaxResponseNegotiator(
849
                array(
850
                    'CurrentForm' => function () use (&$controller) {
851
                        return $controller->getEditForm()->forTemplate();
852
                    },
853
                    'Content' => function () use (&$controller) {
854
                        return $controller->renderWith($controller->getTemplatesWithSuffix('_Content'));
855
                    },
856
                    'Breadcrumbs' => function () use (&$controller) {
857
                        return $controller->renderWith([
858
                            'type' => 'Includes',
859
                            'SilverStripe\\Admin\\CMSBreadcrumbs'
860
                        ]);
861
                    },
862
                    'default' => function () use (&$controller) {
863
                        return $controller->renderWith($controller->getViewer('show'));
864
                    }
865
                ),
866
                $this->getResponse()
867
            );
868
        }
869
        return $this->responseNegotiator;
870
    }
871
872
    //------------------------------------------------------------------------------------------//
873
    // Main UI components
874
875
    /**
876
     * Returns the main menu of the CMS.  This is also used by init()
877
     * to work out which sections the user has access to.
878
     *
879
     * @param bool $cached
880
     * @return SS_List
881
     */
882
    public function MainMenu($cached = true)
883
    {
884
        if (!isset($this->_cache_MainMenu) || !$cached) {
885
            // Don't accidentally return a menu if you're not logged in - it's used to determine access.
886
            if (!Member::currentUser()) {
887
                return new ArrayList();
888
            }
889
890
            // Encode into DO set
891
            $menu = new ArrayList();
892
            $menuItems = CMSMenu::get_viewable_menu_items();
893
894
            // extra styling for custom menu-icons
895
            $menuIconStyling = '';
896
897
            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...
898
                /** @var CMSMenuItem $menuItem */
899
                foreach ($menuItems as $code => $menuItem) {
900
                    // alternate permission checks (in addition to LeftAndMain->canView())
901
                    if (isset($menuItem->controller)
902
                        && $this->hasMethod('alternateMenuDisplayCheck')
903
                        && !$this->alternateMenuDisplayCheck($menuItem->controller)
904
                    ) {
905
                        continue;
906
                    }
907
908
                    $linkingmode = "link";
909
910
                    if ($menuItem->controller && get_class($this) == $menuItem->controller) {
911
                        $linkingmode = "current";
912
                    } elseif (strpos($this->Link(), $menuItem->url) !== false) {
913
                        if ($this->Link() == $menuItem->url) {
914
                            $linkingmode = "current";
915
916
                        // default menu is the one with a blank {@link url_segment}
917
                        } elseif (singleton($menuItem->controller)->stat('url_segment') == '') {
918
                            if ($this->Link() == AdminRootController::admin_url()) {
919
                                $linkingmode = "current";
920
                            }
921
                        } else {
922
                            $linkingmode = "current";
923
                        }
924
                    }
925
926
                    // already set in CMSMenu::populate_menu(), but from a static pre-controller
927
                    // context, so doesn't respect the current user locale in _t() calls - as a workaround,
928
                    // we simply call LeftAndMain::menu_title() again
929
                    // if we're dealing with a controller
930
                    if ($menuItem->controller) {
931
                        $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...
932
                    } else {
933
                        $title = $menuItem->title;
934
                    }
935
936
                    // Provide styling for custom $menu-icon. Done here instead of in
937
                    // CMSMenu::populate_menu(), because the icon is part of
938
                    // the CMS right pane for the specified class as well...
939
                    if ($menuItem->controller) {
940
                        $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...
941
                        if (!empty($menuIcon)) {
942
                            $menuIconStyling .= $menuIcon;
943
                        }
944
                    }
945
946
                    $menu->push(new ArrayData(array(
947
                        "MenuItem" => $menuItem,
948
                        "AttributesHTML" => $menuItem->getAttributesHTML(),
949
                        "Title" => Convert::raw2xml($title),
950
                        "Code" => $code,
951
                        "Icon" => strtolower($code),
952
                        "Link" => $menuItem->url,
953
                        "LinkingMode" => $linkingmode
954
                    )));
955
                }
956
            }
957
            if ($menuIconStyling) {
958
                Requirements::customCSS($menuIconStyling);
959
            }
960
961
            $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...
962
        }
963
964
        return $this->_cache_MainMenu;
965
    }
966
967
    public function Menu()
968
    {
969
        return $this->renderWith($this->getTemplatesWithSuffix('_Menu'));
970
    }
971
972
    /**
973
     * @todo Wrap in CMSMenu instance accessor
974
     * @return ArrayData A single menu entry (see {@link MainMenu})
975
     */
976
    public function MenuCurrentItem()
977
    {
978
        $items = $this->MainMenu();
979
        return $items->find('LinkingMode', 'current');
980
    }
981
982
    /**
983
     * Return a list of appropriate templates for this class, with the given suffix using
984
     * {@link SSViewer::get_templates_by_class()}
985
     *
986
     * @param string $suffix
987
     * @return array
988
     */
989
    public function getTemplatesWithSuffix($suffix)
990
    {
991
        $templates = SSViewer::get_templates_by_class(get_class($this), $suffix, __CLASS__);
992
        return SSViewer::chooseTemplate($templates);
993
    }
994
995
    public function Content()
996
    {
997
        return $this->renderWith($this->getTemplatesWithSuffix('_Content'));
998
    }
999
1000
    /**
1001
     * Render $PreviewPanel content
1002
     *
1003
     * @return DBHTMLText
1004
     */
1005
    public function PreviewPanel()
1006
    {
1007
        $template = $this->getTemplatesWithSuffix('_PreviewPanel');
1008
        // Only render sections with preview panel
1009
        if ($template) {
1010
            return $this->renderWith($template);
1011
        }
1012
    }
1013
1014
    public function getRecord($id)
1015
    {
1016
        $className = $this->stat('tree_class');
1017
        if ($className && $id instanceof $className) {
1018
            return $id;
1019
        } elseif ($className && $id == 'root') {
1020
            return singleton($className);
1021
        } elseif ($className && is_numeric($id)) {
1022
            return DataObject::get_by_id($className, $id);
1023
        } else {
1024
            return false;
1025
        }
1026
    }
1027
1028
    /**
1029
     * @param bool $unlinked
1030
     * @return ArrayList
1031
     */
1032
    public function Breadcrumbs($unlinked = false)
1033
    {
1034
        $items = new ArrayList(array(
1035
            new ArrayData(array(
1036
                'Title' => $this->menu_title(),
1037
                'Link' => ($unlinked) ? false : $this->Link()
1038
            ))
1039
        ));
1040
        $record = $this->currentPage();
1041
        if ($record && $record->exists()) {
1042
            if ($record->hasExtension(Hierarchy::class)) {
1043
                $ancestors = $record->getAncestors();
1044
                $ancestors = new ArrayList(array_reverse($ancestors->toArray()));
1045
                $ancestors->push($record);
1046
                foreach ($ancestors as $ancestor) {
1047
                    $items->push(new ArrayData(array(
1048
                        'Title' => ($ancestor->MenuTitle) ? $ancestor->MenuTitle : $ancestor->Title,
1049
                        'Link' => ($unlinked) ? false : Controller::join_links($this->Link('show'), $ancestor->ID)
1050
                    )));
1051
                }
1052
            } else {
1053
                $items->push(new ArrayData(array(
1054
                    'Title' => ($record->MenuTitle) ? $record->MenuTitle : $record->Title,
1055
                    'Link' => ($unlinked) ? false : Controller::join_links($this->Link('show'), $record->ID)
1056
                )));
1057
            }
1058
        }
1059
1060
        return $items;
1061
    }
1062
1063
    /**
1064
     * @return String HTML
1065
     */
1066
    public function SiteTreeAsUL()
1067
    {
1068
        $html = $this->getSiteTreeFor($this->stat('tree_class'));
1069
        $this->extend('updateSiteTreeAsUL', $html);
1070
        return $html;
1071
    }
1072
1073
    /**
1074
     * Gets the current search filter for this request, if available
1075
     *
1076
     * @throws InvalidArgumentException
1077
     * @return LeftAndMain_SearchFilter
1078
     */
1079
    protected function getSearchFilter()
1080
    {
1081
        // Check for given FilterClass
1082
        $params = $this->getRequest()->getVar('q');
1083
        if (empty($params['FilterClass'])) {
1084
            return null;
1085
        }
1086
1087
        // Validate classname
1088
        $filterClass = $params['FilterClass'];
1089
        $filterInfo = new ReflectionClass($filterClass);
1090
        if (!$filterInfo->implementsInterface('SilverStripe\\Admin\\LeftAndMain_SearchFilter')) {
1091
            throw new InvalidArgumentException(sprintf('Invalid filter class passed: %s', $filterClass));
1092
        }
1093
1094
        return Injector::inst()->createWithArgs($filterClass, array($params));
1095
    }
1096
1097
    /**
1098
     * Get a site tree HTML listing which displays the nodes under the given criteria.
1099
     *
1100
     * @param string $className The class of the root object
1101
     * @param string $rootID The ID of the root object.  If this is null then a complete tree will be
1102
     *  shown
1103
     * @param string $childrenMethod The method to call to get the children of the tree. For example,
1104
     *  Children, AllChildrenIncludingDeleted, or AllHistoricalChildren
1105
     * @param string $numChildrenMethod
1106
     * @param callable $filterFunction
1107
     * @param int $nodeCountThreshold
1108
     * @return string Nested unordered list with links to each page
1109
     */
1110
    public function getSiteTreeFor(
1111
        $className,
1112
        $rootID = null,
1113
        $childrenMethod = null,
1114
        $numChildrenMethod = null,
1115
        $filterFunction = null,
1116
        $nodeCountThreshold = 30
1117
    ) {
1118
1119
        // Filter criteria
1120
        $filter = $this->getSearchFilter();
1121
1122
        // Default childrenMethod and numChildrenMethod
1123
        if (!$childrenMethod) {
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...
1124
            $childrenMethod = ($filter && $filter->getChildrenMethod())
1125
            ? $filter->getChildrenMethod()
1126
            : 'AllChildrenIncludingDeleted';
1127
        }
1128
1129
        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...
1130
            $numChildrenMethod = 'numChildren';
1131
            if ($filter && $filter->getNumChildrenMethod()) {
1132
                $numChildrenMethod = $filter->getNumChildrenMethod();
1133
            }
1134
        }
1135
        if (!$filterFunction && $filter) {
1136
            $filterFunction = function ($node) use ($filter) {
1137
                return $filter->isPageIncluded($node);
1138
            };
1139
        }
1140
1141
        // Get the tree root
1142
        $record = ($rootID) ? $this->getRecord($rootID) : null;
1143
        $obj = $record ? $record : singleton($className);
1144
1145
        // Get the current page
1146
        // NOTE: This *must* be fetched before markPartialTree() is called, as this
1147
        // causes the Hierarchy::$marked cache to be flushed (@see CMSMain::getRecord)
1148
        // which means that deleted pages stored in the marked tree would be removed
1149
        $currentPage = $this->currentPage();
1150
1151
        // Mark the nodes of the tree to return
1152
        if ($filterFunction) {
1153
            $obj->setMarkingFilterFunction($filterFunction);
1154
        }
1155
1156
        $obj->markPartialTree($nodeCountThreshold, $this, $childrenMethod, $numChildrenMethod);
1157
1158
        // Ensure current page is exposed
1159
        if ($currentPage) {
1160
            $obj->markToExpose($currentPage);
1161
        }
1162
1163
        // NOTE: SiteTree/CMSMain coupling :-(
1164
        if (class_exists('SilverStripe\\CMS\\Model\\SiteTree')) {
1165
            SiteTree::prepopulate_permission_cache(
1166
                'CanEditType',
1167
                $obj->markedNodeIDs(),
1168
                'SilverStripe\\CMS\\Model\\SiteTree::can_edit_multiple'
1169
            );
1170
        }
1171
1172
        // getChildrenAsUL is a flexible and complex way of traversing the tree
1173
        $controller = $this;
1174
        $recordController = ($this->stat('tree_class') == 'SilverStripe\\CMS\\Model\\SiteTree')
1175
            ?  CMSPageEditController::singleton()
1176
            : $this;
1177
        $titleFn = function (&$child, $numChildrenMethod) use (&$controller, &$recordController, $filter) {
1178
            $link = Controller::join_links($recordController->Link("show"), $child->ID);
1179
            $node = LeftAndMain_TreeNode::create($child, $link, $controller->isCurrentPage($child), $numChildrenMethod, $filter);
1180
            return $node->forTemplate();
1181
        };
1182
1183
        // Limit the amount of nodes shown for performance reasons.
1184
        // Skip the check if we're filtering the tree, since its not clear how many children will
1185
        // match the filter criteria until they're queried (and matched up with previously marked nodes).
1186
        $nodeThresholdLeaf = Config::inst()->get(Hierarchy::class, 'node_threshold_leaf');
1187
        if ($nodeThresholdLeaf && !$filterFunction) {
1188
            $nodeCountCallback = function ($parent, $numChildren) use (&$controller, $className, $nodeThresholdLeaf) {
1189
                if ($className !== 'SilverStripe\\CMS\\Model\\SiteTree'
1190
                    || !$parent->ID
1191
                    || $numChildren >= $nodeThresholdLeaf
1192
                ) {
1193
                    return null;
1194
                }
1195
                    return sprintf(
1196
                        '<ul><li class="readonly"><span class="item">'
1197
                            . '%s (<a href="%s" class="cms-panel-link" data-pjax-target="Content">%s</a>)'
1198
                            . '</span></li></ul>',
1199
                        _t('LeftAndMain.TooManyPages', 'Too many pages'),
1200
                        Controller::join_links(
1201
                            $controller->LinkWithSearch($controller->Link()),
1202
                            '
1203
							?view=list&ParentID=' . $parent->ID
1204
                        ),
1205
                        _t(
1206
                            'LeftAndMain.ShowAsList',
1207
                            'show as list',
1208
                            'Show large amount of pages in list instead of tree view'
1209
                        )
1210
                    );
1211
            };
1212
        } else {
1213
            $nodeCountCallback = null;
1214
        }
1215
1216
        // If the amount of pages exceeds the node thresholds set, use the callback
1217
        $html = null;
1218
        if ($obj->ParentID && $nodeCountCallback) {
1219
            $html = $nodeCountCallback($obj, $obj->$numChildrenMethod());
1220
        }
1221
1222
        // Otherwise return the actual tree (which might still filter leaf thresholds on children)
1223
        if (!$html) {
1224
            $html = $obj->getChildrenAsUL(
1225
                "",
1226
                $titleFn,
1227
                CMSPagesController::singleton(),
1228
                true,
1229
                $childrenMethod,
1230
                $numChildrenMethod,
1231
                $nodeCountThreshold,
1232
                $nodeCountCallback
1233
            );
1234
        }
1235
1236
        // Wrap the root if needs be.
1237
        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...
1238
            $rootLink = $this->Link('show') . '/root';
1239
1240
            // This lets us override the tree title with an extension
1241
            if ($this->hasMethod('getCMSTreeTitle') && $customTreeTitle = $this->getCMSTreeTitle()) {
1242
                $treeTitle = $customTreeTitle;
1243
            } elseif (class_exists('SilverStripe\\SiteConfig\\SiteConfig')) {
1244
                $siteConfig = SiteConfig::current_site_config();
1245
                $treeTitle =  Convert::raw2xml($siteConfig->Title);
1246
            } else {
1247
                $treeTitle = '...';
1248
            }
1249
1250
            $html = "<ul><li id=\"record-0\" data-id=\"0\" class=\"Root nodelete\"><strong>$treeTitle</strong>"
1251
                . $html . "</li></ul>";
1252
        }
1253
1254
        return $html;
1255
    }
1256
1257
    /**
1258
     * Get a subtree underneath the request param 'ID'.
1259
     * If ID = 0, then get the whole tree.
1260
     *
1261
     * @param HTTPRequest $request
1262
     * @return string
1263
     */
1264
    public function getsubtree($request)
1265
    {
1266
        $html = $this->getSiteTreeFor(
1267
            $this->stat('tree_class'),
1268
            $request->getVar('ID'),
1269
            null,
1270
            null,
1271
            null,
1272
            $request->getVar('minNodeCount')
1273
        );
1274
1275
        // Trim off the outer tag
1276
        $html = preg_replace('/^[\s\t\r\n]*<ul[^>]*>/', '', $html);
1277
        $html = preg_replace('/<\/ul[^>]*>[\s\t\r\n]*$/', '', $html);
1278
1279
        return $html;
1280
    }
1281
1282
    /**
1283
     * Allows requesting a view update on specific tree nodes.
1284
     * Similar to {@link getsubtree()}, but doesn't enforce loading
1285
     * all children with the node. Useful to refresh views after
1286
     * state modifications, e.g. saving a form.
1287
     *
1288
     * @param HTTPRequest $request
1289
     * @return string JSON
1290
     */
1291
    public function updatetreenodes($request)
1292
    {
1293
        $data = array();
1294
        $ids = explode(',', $request->getVar('ids'));
1295
        foreach ($ids as $id) {
1296
            if ($id === "") {
1297
                continue; // $id may be a blank string, which is invalid and should be skipped over
1298
            }
1299
1300
            $record = $this->getRecord($id);
1301
            if (!$record) {
1302
                continue; // In case a page is no longer available
1303
            }
1304
            $recordController = ($this->stat('tree_class') == 'SilverStripe\\CMS\\Model\\SiteTree')
1305
                ? CMSPageEditController::singleton()
1306
                : $this;
1307
1308
            // Find the next & previous nodes, for proper positioning (Sort isn't good enough - it's not a raw offset)
1309
            // TODO: These methods should really be in hierarchy - for a start it assumes Sort exists
1310
            $next = $prev = null;
1311
1312
            $className = $this->stat('tree_class');
1313
            $next = DataObject::get($className)
1314
                ->filter('ParentID', $record->ParentID)
1315
                ->filter('Sort:GreaterThan', $record->Sort)
1316
                ->first();
1317
1318
            if (!$next) {
1319
                $prev = DataObject::get($className)
1320
                    ->filter('ParentID', $record->ParentID)
1321
                    ->filter('Sort:LessThan', $record->Sort)
1322
                    ->reverse()
1323
                    ->first();
1324
            }
1325
1326
            $link = Controller::join_links($recordController->Link("show"), $record->ID);
1327
            $html = LeftAndMain_TreeNode::create($record, $link, $this->isCurrentPage($record))
1328
                ->forTemplate() . '</li>';
1329
1330
            $data[$id] = array(
1331
                'html' => $html,
1332
                'ParentID' => $record->ParentID,
1333
                'NextID' => $next ? $next->ID : null,
1334
                'PrevID' => $prev ? $prev->ID : null
1335
            );
1336
        }
1337
        $this->getResponse()->addHeader('Content-Type', 'text/json');
1338
        return Convert::raw2json($data);
1339
    }
1340
1341
    /**
1342
     * Save  handler
1343
     *
1344
     * @param array $data
1345
     * @param Form $form
1346
     * @return HTTPResponse
1347
     */
1348
    public function save($data, $form)
1349
    {
1350
        $request = $this->getRequest();
1351
        $className = $this->stat('tree_class');
1352
1353
        // Existing or new record?
1354
        $id = $data['ID'];
1355
        if (is_numeric($id) && $id > 0) {
1356
            $record = DataObject::get_by_id($className, $id);
1357
            if ($record && !$record->canEdit()) {
1358
                return Security::permissionFailure($this);
1359
            }
1360
            if (!$record || !$record->ID) {
1361
                $this->httpError(404, "Bad record ID #" . (int)$id);
1362
            }
1363
        } else {
1364
            if (!singleton($this->stat('tree_class'))->canCreate()) {
1365
                return Security::permissionFailure($this);
1366
            }
1367
            $record = $this->getNewItem($id, false);
1368
        }
1369
1370
        // save form data into record
1371
        $form->saveInto($record, true);
1372
        $record->write();
1373
        $this->extend('onAfterSave', $record);
1374
        $this->setCurrentPageID($record->ID);
1375
1376
        $message = _t('LeftAndMain.SAVEDUP', 'Saved.');
1377
        if ($this->getSchemaRequested()) {
1378
            $schemaId = Controller::join_links($this->Link('schema/DetailEditForm'), $id);
1379
            // Ensure that newly created records have all their data loaded back into the form.
1380
            $form->loadDataFrom($record);
1381
            $form->setMessage($message, 'good');
1382
            $response = $this->getSchemaResponse($schemaId, $form);
1383
        } else {
1384
            $response = $this->getResponseNegotiator()->respond($request);
1385
        }
1386
1387
        $response->addHeader('X-Status', rawurlencode($message));
1388
        return $response;
1389
    }
1390
1391
    /**
1392
     * Create new item.
1393
     *
1394
     * @param string|int $id
1395
     * @param bool $setID
1396
     * @return DataObject
1397
     */
1398
    public function getNewItem($id, $setID = true)
1399
    {
1400
        $class = $this->stat('tree_class');
1401
        $object = Injector::inst()->create($class);
1402
        if ($setID) {
1403
            $object->ID = $id;
1404
        }
1405
        return $object;
1406
    }
1407
1408
    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...
1409
    {
1410
        $className = $this->stat('tree_class');
1411
1412
        $id = $data['ID'];
1413
        $record = DataObject::get_by_id($className, $id);
1414
        if ($record && !$record->canDelete()) {
1415
            return Security::permissionFailure();
1416
        }
1417
        if (!$record || !$record->ID) {
1418
            $this->httpError(404, "Bad record ID #" . (int)$id);
1419
        }
1420
1421
        $record->delete();
1422
1423
        $this->getResponse()->addHeader('X-Status', rawurlencode(_t('LeftAndMain.DELETED', 'Deleted.')));
1424
        return $this->getResponseNegotiator()->respond(
1425
            $this->getRequest(),
1426
            array('currentform' => array($this, 'EmptyForm'))
1427
        );
1428
    }
1429
1430
    /**
1431
     * Update the position and parent of a tree node.
1432
     * Only saves the node if changes were made.
1433
     *
1434
     * Required data:
1435
     * - 'ID': The moved node
1436
     * - 'ParentID': New parent relation of the moved node (0 for root)
1437
     * - 'SiblingIDs': Array of all sibling nodes to the moved node (incl. the node itself).
1438
     *   In case of a 'ParentID' change, relates to the new siblings under the new parent.
1439
     *
1440
     * @param HTTPRequest $request
1441
     * @return HTTPResponse JSON string with a
1442
     * @throws HTTPResponse_Exception
1443
     */
1444
    public function savetreenode($request)
1445
    {
1446
        if (!SecurityToken::inst()->checkRequest($request)) {
1447
            return $this->httpError(400);
1448
        }
1449
        if (!Permission::check('SITETREE_REORGANISE') && !Permission::check('ADMIN')) {
1450
            $this->getResponse()->setStatusCode(
1451
                403,
1452
                _t(
1453
                    'LeftAndMain.CANT_REORGANISE',
1454
                    "You do not have permission to rearange the site tree. Your change was not saved."
1455
                )
1456
            );
1457
            return;
1458
        }
1459
1460
        $className = $this->stat('tree_class');
1461
        $statusUpdates = array('modified'=>array());
1462
        $id = $request->requestVar('ID');
1463
        $parentID = $request->requestVar('ParentID');
1464
1465
        if ($className == 'SilverStripe\\CMS\\Model\\SiteTree' && $page = DataObject::get_by_id('Page', $id)) {
1466
            $root = $page->getParentType();
1467
            if (($parentID == '0' || $root == 'root') && !SiteConfig::current_site_config()->canCreateTopLevel()) {
1468
                $this->getResponse()->setStatusCode(
1469
                    403,
1470
                    _t(
1471
                        'LeftAndMain.CANT_REORGANISE',
1472
                        "You do not have permission to alter Top level pages. Your change was not saved."
1473
                    )
1474
                );
1475
                return;
1476
            }
1477
        }
1478
1479
        $siblingIDs = $request->requestVar('SiblingIDs');
1480
        $statusUpdates = array('modified'=>array());
1481
        if (!is_numeric($id) || !is_numeric($parentID)) {
1482
            throw new InvalidArgumentException();
1483
        }
1484
1485
        $node = DataObject::get_by_id($className, $id);
1486
        if ($node && !$node->canEdit()) {
1487
            return Security::permissionFailure($this);
1488
        }
1489
1490
        if (!$node) {
1491
            $this->getResponse()->setStatusCode(
1492
                500,
1493
                _t(
1494
                    'LeftAndMain.PLEASESAVE',
1495
                    "Please Save Page: This page could not be updated because it hasn't been saved yet."
1496
                )
1497
            );
1498
            return;
1499
        }
1500
1501
        // Update hierarchy (only if ParentID changed)
1502
        if ($node->ParentID != $parentID) {
1503
            $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...
1504
            $node->write();
1505
1506
            $statusUpdates['modified'][$node->ID] = array(
1507
                'TreeTitle'=>$node->TreeTitle
1508
            );
1509
1510
            // Update all dependent pages
1511
            if (class_exists('SilverStripe\\CMS\\Model\\VirtualPage')) {
1512
                $virtualPages = VirtualPage::get()->filter("CopyContentFromID", $node->ID);
1513
                foreach ($virtualPages as $virtualPage) {
1514
                    $statusUpdates['modified'][$virtualPage->ID] = array(
1515
                        'TreeTitle' => $virtualPage->TreeTitle()
1516
                    );
1517
                }
1518
            }
1519
1520
            $this->getResponse()->addHeader(
1521
                'X-Status',
1522
                rawurlencode(_t('LeftAndMain.REORGANISATIONSUCCESSFUL', 'Reorganised the site tree successfully.'))
1523
            );
1524
        }
1525
1526
        // Update sorting
1527
        if (is_array($siblingIDs)) {
1528
            $counter = 0;
1529
            foreach ($siblingIDs as $id) {
1530
                if ($id == $node->ID) {
1531
                    $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...
1532
                    $node->write();
1533
                    $statusUpdates['modified'][$node->ID] = array(
1534
                        'TreeTitle' => $node->TreeTitle
1535
                    );
1536
                } elseif (is_numeric($id)) {
1537
                    // Nodes that weren't "actually moved" shouldn't be registered as
1538
                    // having been edited; do a direct SQL update instead
1539
                    ++$counter;
1540
                    $table = DataObject::getSchema()->baseDataTable($className);
1541
                    DB::prepared_query(
1542
                        "UPDATE \"$table\" SET \"Sort\" = ? WHERE \"ID\" = ?",
1543
                        array($counter, $id)
1544
                    );
1545
                }
1546
            }
1547
1548
            $this->getResponse()->addHeader(
1549
                'X-Status',
1550
                rawurlencode(_t('LeftAndMain.REORGANISATIONSUCCESSFUL', 'Reorganised the site tree successfully.'))
1551
            );
1552
        }
1553
1554
        return Convert::raw2json($statusUpdates);
1555
    }
1556
1557
    public function CanOrganiseSitetree()
1558
    {
1559
        return !Permission::check('SITETREE_REORGANISE') && !Permission::check('ADMIN') ? false : true;
1560
    }
1561
1562
    /**
1563
     * Retrieves an edit form, either for display, or to process submitted data.
1564
     * Also used in the template rendered through {@link Right()} in the $EditForm placeholder.
1565
     *
1566
     * This is a "pseudo-abstract" methoed, usually connected to a {@link getEditForm()}
1567
     * method in an entwine subclass. This method can accept a record identifier,
1568
     * selected either in custom logic, or through {@link currentPageID()}.
1569
     * The form usually construct itself from {@link DataObject->getCMSFields()}
1570
     * for the specific managed subclass defined in {@link LeftAndMain::$tree_class}.
1571
     *
1572
     * @param HTTPRequest $request Optionally contains an identifier for the
1573
     *  record to load into the form.
1574
     * @return Form Should return a form regardless wether a record has been found.
1575
     *  Form might be readonly if the current user doesn't have the permission to edit
1576
     *  the record.
1577
     */
1578
    /**
1579
     * @return Form
1580
     */
1581
    public function EditForm($request = null)
1582
    {
1583
        return $this->getEditForm();
1584
    }
1585
1586
    /**
1587
     * Calls {@link SiteTree->getCMSFields()}
1588
     *
1589
     * @param Int $id
1590
     * @param FieldList $fields
1591
     * @return Form
1592
     */
1593
    public function getEditForm($id = null, $fields = null)
1594
    {
1595
        if (!$id) {
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...
1596
            $id = $this->currentPageID();
1597
        }
1598
1599
        if (is_object($id)) {
1600
            $record = $id;
1601
        } else {
1602
            $record = $this->getRecord($id);
1603
            if ($record && !$record->canView()) {
1604
                return Security::permissionFailure($this);
1605
            }
1606
        }
1607
1608
        if ($record) {
1609
            $fields = ($fields) ? $fields : $record->getCMSFields();
1610
            if ($fields == null) {
1611
                user_error(
1612
                    "getCMSFields() returned null  - it should return a FieldList object.
1613
					Perhaps you forgot to put a return statement at the end of your method?",
1614
                    E_USER_ERROR
1615
                );
1616
            }
1617
1618
            // Add hidden fields which are required for saving the record
1619
            // and loading the UI state
1620
            if (!$fields->dataFieldByName('ClassName')) {
1621
                $fields->push(new HiddenField('ClassName'));
1622
            }
1623
1624
            $tree_class = $this->stat('tree_class');
1625
            if ($tree_class::has_extension(Hierarchy::class)
1626
                && !$fields->dataFieldByName('ParentID')
1627
            ) {
1628
                $fields->push(new HiddenField('ParentID'));
1629
            }
1630
1631
            // Added in-line to the form, but plucked into different view by frontend scripts.
1632
            if ($record instanceof CMSPreviewable) {
1633
                /** @skipUpgrade */
1634
                $navField = new LiteralField('SilverStripeNavigator', $this->getSilverStripeNavigator());
1635
                $navField->setAllowHTML(true);
1636
                $fields->push($navField);
1637
            }
1638
1639
            if ($record->hasMethod('getAllCMSActions')) {
1640
                $actions = $record->getAllCMSActions();
1641
            } else {
1642
                $actions = $record->getCMSActions();
1643
                // add default actions if none are defined
1644
                if (!$actions || !$actions->count()) {
1645
                    if ($record->hasMethod('canEdit') && $record->canEdit()) {
1646
                        $actions->push(
1647
                            FormAction::create('save', _t('CMSMain.SAVE', 'Save'))
1648
                                ->addExtraClass('ss-ui-action-constructive')->setAttribute('data-icon', 'accept')
1649
                        );
1650
                    }
1651
                    if ($record->hasMethod('canDelete') && $record->canDelete()) {
1652
                        $actions->push(
1653
                            FormAction::create('delete', _t('ModelAdmin.DELETE', 'Delete'))
1654
                                ->addExtraClass('ss-ui-action-destructive')
1655
                        );
1656
                    }
1657
                }
1658
            }
1659
1660
            // Use <button> to allow full jQuery UI styling
1661
            $actionsFlattened = $actions->dataFields();
1662
            if ($actionsFlattened) {
1663
                /** @var FormAction $action */
1664
                foreach ($actionsFlattened as $action) {
1665
                    $action->setUseButtonTag(true);
1666
                }
1667
            }
1668
1669
            $negotiator = $this->getResponseNegotiator();
1670
            $form = Form::create(
1671
                $this,
1672
                "EditForm",
1673
                $fields,
1674
                $actions
1675
            )->setHTMLID('Form_EditForm');
1676
            $form->addExtraClass('cms-edit-form');
1677
            $form->loadDataFrom($record);
1678
            $form->setTemplate($this->getTemplatesWithSuffix('_EditForm'));
1679
            $form->setAttribute('data-pjax-fragment', 'CurrentForm');
1680
            $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...
1681
                $request = $this->getRequest();
1682
                if ($request->isAjax() && $negotiator) {
1683
                    $result = $form->forTemplate();
1684
1685
                    return $negotiator->respond($request, array(
1686
                        'CurrentForm' => function () use ($result) {
1687
                            return $result;
1688
                        }
1689
                    ));
1690
                }
1691
            });
1692
1693
            // Announce the capability so the frontend can decide whether to allow preview or not.
1694
            if ($record instanceof CMSPreviewable) {
1695
                $form->addExtraClass('cms-previewable');
1696
            }
1697
            $form->addExtraClass('fill-height');
1698
1699
            // Set this if you want to split up tabs into a separate header row
1700
            // 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...
1701
            // 	$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...
1702
            // }
1703
1704
            // Add a default or custom validator.
1705
            // @todo Currently the default Validator.js implementation
1706
            //  adds javascript to the document body, meaning it won't
1707
            //  be included properly if the associated fields are loaded
1708
            //  through ajax. This means only serverside validation
1709
            //  will kick in for pages+validation loaded through ajax.
1710
            //  This will be solved by using less obtrusive javascript validation
1711
            //  in the future, see http://open.silverstripe.com/ticket/2915 and
1712
            //  http://open.silverstripe.com/ticket/3386
1713
            if ($record->hasMethod('getCMSValidator')) {
1714
                $validator = $record->getCMSValidator();
1715
                // The clientside (mainly LeftAndMain*.js) rely on ajax responses
1716
                // which can be evaluated as javascript, hence we need
1717
                // to override any global changes to the validation handler.
1718
                if ($validator != null) {
1719
                    $form->setValidator($validator);
1720
                }
1721
            } else {
1722
                $form->unsetValidator();
1723
            }
1724
1725
            if ($record->hasMethod('canEdit') && !$record->canEdit()) {
1726
                $readonlyFields = $form->Fields()->makeReadonly();
1727
                $form->setFields($readonlyFields);
1728
            }
1729
        } else {
1730
            $form = $this->EmptyForm();
1731
        }
1732
1733
        return $form;
1734
    }
1735
1736
    /**
1737
     * Returns a placeholder form, used by {@link getEditForm()} if no record is selected.
1738
     * Our javascript logic always requires a form to be present in the CMS interface.
1739
     *
1740
     * @return Form
1741
     */
1742
    public function EmptyForm()
1743
    {
1744
        $form = Form::create(
1745
            $this,
1746
            "EditForm",
1747
            new FieldList(
1748
                // new HeaderField(
1749
                // 	'WelcomeHeader',
1750
                // 	$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...
1751
                // ),
1752
                // new LiteralField(
1753
                // 	'WelcomeText',
1754
                // 	sprintf('<p id="WelcomeMessage">%s %s. %s</p>',
1755
                // 		_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...
1756
                // 		$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...
1757
                // 		_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...
1758
                // 	)
1759
                // )
1760
            ),
1761
            new FieldList()
1762
        )->setHTMLID('Form_EditForm');
1763
        $form->unsetValidator();
1764
        $form->addExtraClass('cms-edit-form');
1765
        $form->addExtraClass('root-form');
1766
        $form->setTemplate($this->getTemplatesWithSuffix('_EditForm'));
1767
        $form->setAttribute('data-pjax-fragment', 'CurrentForm');
1768
1769
        return $form;
1770
    }
1771
1772
    /**
1773
     * Return the CMS's HTML-editor toolbar
1774
     */
1775
    public function EditorToolbar()
1776
    {
1777
        return HTMLEditorField_Toolbar::create($this, "EditorToolbar");
1778
    }
1779
1780
    /**
1781
     * Renders a panel containing tools which apply to all displayed
1782
     * "content" (mostly through {@link EditForm()}), for example a tree navigation or a filter panel.
1783
     * Auto-detects applicable templates by naming convention: "<controller classname>_Tools.ss",
1784
     * and takes the most specific template (see {@link getTemplatesWithSuffix()}).
1785
     * To explicitly disable the panel in the subclass, simply create a more specific, empty template.
1786
     *
1787
     * @return String HTML
1788
     */
1789
    public function Tools()
1790
    {
1791
        $templates = $this->getTemplatesWithSuffix('_Tools');
1792
        if ($templates) {
1793
            $viewer = new SSViewer($templates);
1794
            return $viewer->process($this);
1795
        } else {
1796
            return false;
1797
        }
1798
    }
1799
1800
    /**
1801
     * Renders a panel containing tools which apply to the currently displayed edit form.
1802
     * The main difference to {@link Tools()} is that the panel is displayed within
1803
     * the element structure of the form panel (rendered through {@link EditForm}).
1804
     * This means the panel will be loaded alongside new forms, and refreshed upon save,
1805
     * which can mean a performance hit, depending on how complex your panel logic gets.
1806
     * Any form fields contained in the returned markup will also be submitted with the main form,
1807
     * which might be desired depending on the implementation details.
1808
     *
1809
     * @return String HTML
1810
     */
1811
    public function EditFormTools()
1812
    {
1813
        $templates = $this->getTemplatesWithSuffix('_EditFormTools');
1814
        if ($templates) {
1815
            $viewer = new SSViewer($templates);
1816
            return $viewer->process($this);
1817
        } else {
1818
            return false;
1819
        }
1820
    }
1821
1822
    /**
1823
     * Batch Actions Handler
1824
     */
1825
    public function batchactions()
1826
    {
1827
        return new CMSBatchActionHandler($this, 'batchactions', $this->stat('tree_class'));
1828
    }
1829
1830
    /**
1831
     * @return Form
1832
     */
1833
    public function BatchActionsForm()
1834
    {
1835
        $actions = $this->batchactions()->batchActionList();
1836
        $actionsMap = array('-1' => _t('LeftAndMain.DropdownBatchActionsDefault', 'Choose an action...')); // Placeholder action
1837
        foreach ($actions as $action) {
1838
            $actionsMap[$action->Link] = $action->Title;
1839
        }
1840
1841
        $form = new Form(
1842
            $this,
1843
            'BatchActionsForm',
1844
            new FieldList(
1845
                new HiddenField('csvIDs'),
1846
                DropdownField::create(
1847
                    'Action',
1848
                    false,
1849
                    $actionsMap
1850
                )
1851
                    ->setAttribute('autocomplete', 'off')
1852
                    ->setAttribute('data-placeholder', _t('LeftAndMain.DropdownBatchActionsDefault', 'Choose an action...'))
1853
            ),
1854
            new FieldList(
1855
                // TODO i18n
1856
                new FormAction('submit', _t('Form.SubmitBtnLabel', "Go"))
1857
            )
1858
        );
1859
        $form->addExtraClass('cms-batch-actions form--no-dividers');
1860
        $form->unsetValidator();
1861
1862
        $this->extend('updateBatchActionsForm', $form);
1863
        return $form;
1864
    }
1865
1866
    public function printable()
1867
    {
1868
        $form = $this->getEditForm($this->currentPageID());
1869
        if (!$form) {
1870
            return false;
1871
        }
1872
1873
        $form->transform(new PrintableTransformation());
1874
        $form->setActions(null);
1875
1876
        Requirements::clear();
1877
        Requirements::css(FRAMEWORK_ADMIN_DIR . '/dist/css/LeftAndMain_printable.css');
1878
        return array(
1879
            "PrintForm" => $form
1880
        );
1881
    }
1882
1883
    /**
1884
     * Used for preview controls, mainly links which switch between different states of the page.
1885
     *
1886
     * @return DBHTMLText
1887
     */
1888
    public function getSilverStripeNavigator()
1889
    {
1890
        $page = $this->currentPage();
1891
        if ($page instanceof CMSPreviewable) {
1892
            $navigator = new SilverStripeNavigator($page);
1893
            return $navigator->renderWith($this->getTemplatesWithSuffix('_SilverStripeNavigator'));
1894
        }
1895
        return null;
1896
    }
1897
1898
    /**
1899
     * Identifier for the currently shown record,
1900
     * in most cases a database ID. Inspects the following
1901
     * sources (in this order):
1902
     * - GET/POST parameter named 'ID'
1903
     * - URL parameter named 'ID'
1904
     * - Session value namespaced by classname, e.g. "CMSMain.currentPage"
1905
     *
1906
     * @return int
1907
     */
1908
    public function currentPageID()
1909
    {
1910
        if ($this->getRequest()->requestVar('ID') && is_numeric($this->getRequest()->requestVar('ID'))) {
1911
            return $this->getRequest()->requestVar('ID');
1912
        } elseif ($this->getRequest()->requestVar('CMSMainCurrentPageID') && is_numeric($this->getRequest()->requestVar('CMSMainCurrentPageID'))) {
1913
            // see GridFieldDetailForm::ItemEditForm
1914
            return $this->getRequest()->requestVar('CMSMainCurrentPageID');
1915
        } elseif (isset($this->urlParams['ID']) && is_numeric($this->urlParams['ID'])) {
1916
            return $this->urlParams['ID'];
1917
        } elseif (Session::get($this->sessionNamespace() . ".currentPage")) {
1918
            return Session::get($this->sessionNamespace() . ".currentPage");
1919
        } else {
1920
            return null;
1921
        }
1922
    }
1923
1924
    /**
1925
     * Forces the current page to be set in session,
1926
     * which can be retrieved later through {@link currentPageID()}.
1927
     * Keep in mind that setting an ID through GET/POST or
1928
     * as a URL parameter will overrule this value.
1929
     *
1930
     * @param int $id
1931
     */
1932
    public function setCurrentPageID($id)
1933
    {
1934
        $id = (int)$id;
1935
        Session::set($this->sessionNamespace() . ".currentPage", $id);
1936
    }
1937
1938
    /**
1939
     * Uses {@link getRecord()} and {@link currentPageID()}
1940
     * to get the currently selected record.
1941
     *
1942
     * @return DataObject
1943
     */
1944
    public function currentPage()
1945
    {
1946
        return $this->getRecord($this->currentPageID());
1947
    }
1948
1949
    /**
1950
     * Compares a given record to the currently selected one (if any).
1951
     * Used for marking the current tree node.
1952
     *
1953
     * @param DataObject $record
1954
     * @return bool
1955
     */
1956
    public function isCurrentPage(DataObject $record)
1957
    {
1958
        return ($record->ID == $this->currentPageID());
1959
    }
1960
1961
    /**
1962
     * @return String
1963
     */
1964
    protected function sessionNamespace()
1965
    {
1966
        $override = $this->stat('session_namespace');
1967
        return $override ? $override : $this->class;
1968
    }
1969
1970
    /**
1971
     * URL to a previewable record which is shown through this controller.
1972
     * The controller might not have any previewable content, in which case
1973
     * this method returns FALSE.
1974
     *
1975
     * @return String|boolean
1976
     */
1977
    public function LinkPreview()
1978
    {
1979
        return false;
1980
    }
1981
1982
    /**
1983
     * Return the version number of this application.
1984
     * Uses the number in <mymodule>/silverstripe_version
1985
     * (automatically replaced by build scripts).
1986
     * If silverstripe_version is empty,
1987
     * then attempts to get it from composer.lock
1988
     *
1989
     * @return string
1990
     */
1991
    public function CMSVersion()
1992
    {
1993
        $versions = array();
1994
        $modules = array(
1995
            'silverstripe/framework' => array(
1996
                'title' => 'Framework',
1997
                'versionFile' => FRAMEWORK_PATH . '/silverstripe_version',
1998
            )
1999
        );
2000
        if (defined('CMS_PATH')) {
2001
            $modules['silverstripe/cms'] = array(
2002
                'title' => 'CMS',
2003
                'versionFile' => CMS_PATH . '/silverstripe_version',
2004
            );
2005
        }
2006
2007
        // Tries to obtain version number from composer.lock if it exists
2008
        $composerLockPath = BASE_PATH . '/composer.lock';
2009
        if (file_exists($composerLockPath)) {
2010
            $cache = Cache::factory('LeftAndMain_CMSVersion');
2011
            $cacheKey = filemtime($composerLockPath);
2012
            $versions = $cache->load($cacheKey);
2013
            if ($versions) {
2014
                $versions = json_decode($versions, true);
2015
            } else {
2016
                $versions = array();
2017
            }
2018
            if (!$versions && $jsonData = file_get_contents($composerLockPath)) {
2019
                $lockData = json_decode($jsonData);
2020
                if ($lockData && isset($lockData->packages)) {
2021
                    foreach ($lockData->packages as $package) {
2022
                        if (array_key_exists($package->name, $modules)
2023
                            && isset($package->version)
2024
                        ) {
2025
                            $versions[$package->name] = $package->version;
2026
                        }
2027
                    }
2028
                    $cache->save(json_encode($versions), $cacheKey);
2029
                }
2030
            }
2031
        }
2032
2033
        // Fall back to static version file
2034
        foreach ($modules as $moduleName => $moduleSpec) {
2035
            if (!isset($versions[$moduleName])) {
2036
                if ($staticVersion = file_get_contents($moduleSpec['versionFile'])) {
2037
                    $versions[$moduleName] = $staticVersion;
2038
                } else {
2039
                    $versions[$moduleName] = _t('LeftAndMain.VersionUnknown', 'Unknown');
2040
                }
2041
            }
2042
        }
2043
2044
        $out = array();
2045
        foreach ($modules as $moduleName => $moduleSpec) {
2046
            $out[] = $modules[$moduleName]['title'] . ': ' . $versions[$moduleName];
2047
        }
2048
        return implode(', ', $out);
2049
    }
2050
2051
    /**
2052
     * @return array
2053
     */
2054
    public function SwitchView()
2055
    {
2056
        if ($page = $this->currentPage()) {
2057
            $nav = SilverStripeNavigator::get_for_record($page);
2058
            return $nav['items'];
2059
        }
2060
    }
2061
2062
    /**
2063
     * @return SiteConfig
2064
     */
2065
    public function SiteConfig()
2066
    {
2067
        return (class_exists('SilverStripe\\SiteConfig\\SiteConfig')) ? SiteConfig::current_site_config() : null;
2068
    }
2069
2070
    /**
2071
     * The href for the anchor on the Silverstripe logo.
2072
     * Set by calling LeftAndMain::set_application_link()
2073
     *
2074
     * @config
2075
     * @var String
2076
     */
2077
    private static $application_link = '//www.silverstripe.org/';
2078
2079
    /**
2080
     * @return String
2081
     */
2082
    public function ApplicationLink()
2083
    {
2084
        return $this->stat('application_link');
2085
    }
2086
2087
    /**
2088
     * The application name. Customisable by calling
2089
     * LeftAndMain::setApplicationName() - the first parameter.
2090
     *
2091
     * @config
2092
     * @var String
2093
     */
2094
    private static $application_name = 'SilverStripe';
2095
2096
    /**
2097
     * Get the application name.
2098
     *
2099
     * @return string
2100
     */
2101
    public function getApplicationName()
2102
    {
2103
        return $this->stat('application_name');
2104
    }
2105
2106
    /**
2107
     * @return string
2108
     */
2109
    public function Title()
2110
    {
2111
        $app = $this->getApplicationName();
2112
2113
        return ($section = $this->SectionTitle()) ? sprintf('%s - %s', $app, $section) : $app;
2114
    }
2115
2116
    /**
2117
     * Return the title of the current section. Either this is pulled from
2118
     * the current panel's menu_title or from the first active menu
2119
     *
2120
     * @return string
2121
     */
2122
    public function SectionTitle()
2123
    {
2124
        $title = $this->menu_title();
2125
        if ($title) {
2126
            return $title;
2127
        }
2128
2129
        foreach ($this->MainMenu() as $menuItem) {
2130
            if ($menuItem->LinkingMode != 'link') {
2131
                return $menuItem->Title;
2132
            }
2133
        }
2134
    }
2135
2136
    /**
2137
     * Same as {@link ViewableData->CSSClasses()}, but with a changed name
2138
     * to avoid problems when using {@link ViewableData->customise()}
2139
     * (which always returns "ArrayData" from the $original object).
2140
     *
2141
     * @return String
2142
     */
2143
    public function BaseCSSClasses()
2144
    {
2145
        return $this->CSSClasses('SilverStripe\\Control\\Controller');
2146
    }
2147
2148
    /**
2149
     * @return String
2150
     */
2151
    public function Locale()
2152
    {
2153
        return DBField::create_field('Locale', i18n::get_locale());
2154
    }
2155
2156
    public function providePermissions()
2157
    {
2158
        $perms = array(
2159
            "CMS_ACCESS_LeftAndMain" => array(
2160
                'name' => _t('CMSMain.ACCESSALLINTERFACES', 'Access to all CMS sections'),
2161
                'category' => _t('Permission.CMS_ACCESS_CATEGORY', 'CMS Access'),
2162
                'help' => _t('CMSMain.ACCESSALLINTERFACESHELP', 'Overrules more specific access settings.'),
2163
                'sort' => -100
2164
            )
2165
        );
2166
2167
        // Add any custom ModelAdmin subclasses. Can't put this on ModelAdmin itself
2168
        // since its marked abstract, and needs to be singleton instanciated.
2169
        foreach (ClassInfo::subclassesFor('SilverStripe\\Admin\\ModelAdmin') as $i => $class) {
2170
            if ($class == 'SilverStripe\\Admin\\ModelAdmin') {
2171
                continue;
2172
            }
2173
            if (ClassInfo::classImplements($class, 'SilverStripe\\Dev\\TestOnly')) {
2174
                continue;
2175
            }
2176
2177
            // Check if modeladmin has explicit required_permission_codes option.
2178
            // If a modeladmin is namespaced you can apply this config to override
2179
            // the default permission generation based on fully qualified class name.
2180
            $code = $this->getRequiredPermissions();
2181
            if (!$code) {
2182
                continue;
2183
            }
2184
            // Get first permission if multiple specified
2185
            if (is_array($code)) {
2186
                $code = reset($code);
2187
            }
2188
            $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...
2189
            $perms[$code] = array(
2190
                'name' => _t(
2191
                    'CMSMain.ACCESS',
2192
                    "Access to '{title}' section",
2193
                    "Item in permission selection identifying the admin section. Example: Access to 'Files & Images'",
2194
                    array('title' => $title)
2195
                ),
2196
                'category' => _t('Permission.CMS_ACCESS_CATEGORY', 'CMS Access')
2197
            );
2198
        }
2199
2200
        return $perms;
2201
    }
2202
}
2203