Passed
Push — master ( 9a8bff...6edc41 )
by Thomas
02:55
created

LeftAndMain::getEditForm()   F

Complexity

Conditions 20
Paths 390

Size

Total Lines 89
Code Lines 55

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 20
eloc 55
c 1
b 0
f 0
nc 390
nop 2
dl 0
loc 89
rs 0.9583

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
namespace LeKoala\Admini;
4
5
use LogicException;
6
use BadMethodCallException;
7
use SilverStripe\i18n\i18n;
8
use SilverStripe\Forms\Form;
9
use SilverStripe\ORM\SS_List;
10
use SilverStripe\Dev\TestOnly;
11
use SilverStripe\ORM\ArrayList;
12
use SilverStripe\View\SSViewer;
13
use SilverStripe\Control\Cookie;
14
use SilverStripe\Core\ClassInfo;
15
use SilverStripe\ORM\DataObject;
16
use SilverStripe\View\ArrayData;
17
use LeKoala\Admini\Traits\Toasts;
18
use SilverStripe\Dev\Deprecation;
19
use SilverStripe\Forms\FieldList;
20
use SilverStripe\Security\Member;
21
use SilverStripe\Control\Director;
22
use SilverStripe\Forms\FormAction;
23
use SilverStripe\Forms\HiddenField;
24
use SilverStripe\Security\Security;
25
use SilverStripe\View\Requirements;
26
use LeKoala\Tabulator\TabulatorGrid;
27
use SilverStripe\Control\Controller;
28
use SilverStripe\Core\Config\Config;
29
use LeKoala\DeferBackend\CspProvider;
30
use SilverStripe\Control\HTTPRequest;
31
use SilverStripe\Security\Permission;
32
use SilverStripe\Versioned\Versioned;
33
use LeKoala\DeferBackend\DeferBackend;
34
use SilverStripe\Control\HTTPResponse;
35
use SilverStripe\ORM\ValidationResult;
36
use LeKoala\Admini\Traits\JsonResponse;
37
use SilverStripe\ORM\FieldType\DBField;
38
use SilverStripe\SiteConfig\SiteConfig;
39
use SilverStripe\Core\Injector\Injector;
40
use SilverStripe\Security\SecurityToken;
41
use SilverStripe\ORM\Hierarchy\Hierarchy;
42
use SilverStripe\ORM\ValidationException;
43
use SilverStripe\ORM\FieldType\DBHTMLText;
44
use SilverStripe\Control\ContentNegotiator;
45
use SilverStripe\Core\Manifest\ModuleLoader;
46
use SilverStripe\Security\PermissionProvider;
47
use SilverStripe\Core\Manifest\VersionProvider;
48
use SilverStripe\Forms\PrintableTransformation;
49
use SilverStripe\Control\HTTPResponse_Exception;
50
use SilverStripe\Control\Middleware\HTTPCacheControlMiddleware;
51
52
/**
53
 * LeftAndMain is the parent class of all the two-pane views in the CMS.
54
 * If you are wanting to add more areas to the CMS, you can do it by subclassing LeftAndMain.
55
 *
56
 * This is essentially an abstract class which should be subclassed.
57
 *
58
 * @method bool alternateMenuDisplayCheck(Member $member = null)
59
 * @method bool alternateAccessCheck(Member $member = null)
60
 */
61
class LeftAndMain extends Controller implements PermissionProvider
62
{
63
    use JsonResponse;
64
    use Toasts;
65
66
    /**
67
     * The current url segment attached to the LeftAndMain instance
68
     *
69
     * @config
70
     * @var string
71
     */
72
    private static $url_segment = null;
73
74
    /**
75
     * @config
76
     * @var string Used by {@link AdminiRootController} to augment Director route rules for sub-classes of LeftAndMain
77
     */
78
    private static $url_rule = '/$Action/$ID/$OtherID';
79
80
    /**
81
     * @config
82
     * @var string
83
     */
84
    private static $menu_title;
85
86
    /**
87
     * An icon name for last-icon. You can check MaterialIcons class for values
88
     * @config
89
     * @var string
90
     */
91
    private static $menu_icon;
0 ignored issues
show
introduced by
The private property $menu_icon is not used, and could be removed.
Loading history...
92
93
    /**
94
     * @config
95
     * @var int
96
     */
97
    private static $menu_priority = 0;
0 ignored issues
show
introduced by
The private property $menu_priority is not used, and could be removed.
Loading history...
98
99
    /**
100
     * @config
101
     * @var int
102
     */
103
    private static $url_priority = 50;
0 ignored issues
show
introduced by
The private property $url_priority is not used, and could be removed.
Loading history...
104
105
    /**
106
     * A subclass of {@link DataObject}.
107
     *
108
     * Determines what is managed in this interface, through
109
     * {@link getEditForm()} and other logic.
110
     *
111
     * @config
112
     * @var string
113
     */
114
    private static $tree_class = null;
115
116
    /**
117
     * @var array
118
     */
119
    private static $allowed_actions = [
120
        'index',
121
        'save',
122
        'printable',
123
        'show',
124
        'EditForm',
125
    ];
126
127
    /**
128
     * Assign themes to use for cms
129
     *
130
     * @config
131
     * @var array
132
     */
133
    private static $admin_themes = [
0 ignored issues
show
introduced by
The private property $admin_themes is not used, and could be removed.
Loading history...
134
        'lekoala/silverstripe-admini:forms',
135
        SSViewer::DEFAULT_THEME,
136
    ];
137
138
    /**
139
     * Codes which are required from the current user to view this controller.
140
     * If multiple codes are provided, all of them are required.
141
     * All CMS controllers require "CMS_ACCESS_LeftAndMain" as a baseline check,
142
     * and fall back to "CMS_ACCESS_<class>" if no permissions are defined here.
143
     * See {@link canView()} for more details on permission checks.
144
     *
145
     * @config
146
     * @var array
147
     */
148
    private static $required_permission_codes;
149
150
    /**
151
     * Namespace for session info, e.g. current record.
152
     * Defaults to the current class name, but can be amended to share a namespace in case
153
     * controllers are logically bundled together, and mainly separated
154
     * to achieve more flexible templating.
155
     *
156
     * @config
157
     * @var string
158
     */
159
    private static $session_namespace;
0 ignored issues
show
introduced by
The private property $session_namespace is not used, and could be removed.
Loading history...
160
161
    /**
162
     * Register additional requirements through the {@link Requirements} class.
163
     * Used mainly to work around the missing "lazy loading" functionality
164
     * for getting css/javascript required after an ajax-call (e.g. loading the editform).
165
     *
166
     * YAML configuration example:
167
     * <code>
168
     * LeftAndMain:
169
     *   extra_requirements_javascript:
170
     *     - mysite/javascript/myscript.js
171
     * </code>
172
     *
173
     * @config
174
     * @var array
175
     */
176
    private static $extra_requirements_javascript = [];
0 ignored issues
show
introduced by
The private property $extra_requirements_javascript is not used, and could be removed.
Loading history...
177
178
    /**
179
     * YAML configuration example:
180
     * <code>
181
     * LeftAndMain:
182
     *   extra_requirements_css:
183
     *     mysite/css/mystyle.css:
184
     *       media: screen
185
     * </code>
186
     *
187
     * @config
188
     * @var array See {@link extra_requirements_javascript}
189
     */
190
    private static $extra_requirements_css = [];
0 ignored issues
show
introduced by
The private property $extra_requirements_css is not used, and could be removed.
Loading history...
191
192
    /**
193
     * @config
194
     * @var array See {@link extra_requirements_javascript}
195
     */
196
    private static $extra_requirements_themedCss = [];
0 ignored issues
show
introduced by
The private property $extra_requirements_themedCss is not used, and could be removed.
Loading history...
197
198
    /**
199
     * If true, call a keepalive ping every 5 minutes from the CMS interface,
200
     * to ensure that the session never dies.
201
     *
202
     * @config
203
     * @var bool
204
     */
205
    private static $session_keepalive_ping = true;
0 ignored issues
show
introduced by
The private property $session_keepalive_ping is not used, and could be removed.
Loading history...
206
207
    /**
208
     * Value of X-Frame-Options header
209
     *
210
     * @config
211
     * @var string
212
     */
213
    private static $frame_options = 'SAMEORIGIN';
0 ignored issues
show
introduced by
The private property $frame_options is not used, and could be removed.
Loading history...
214
215
    /**
216
     * The configuration passed to the supporting JS for each CMS section includes a 'name' key
217
     * that by default matches the FQCN of the current class. This setting allows you to change
218
     * the key if necessary (for example, if you are overloading CMSMain or another core class
219
     * and want to keep the core JS - which depends on the core class names - functioning, you
220
     * would need to set this to the FQCN of the class you are overloading).
221
     *
222
     * @config
223
     * @var string|null
224
     */
225
    private static $section_name = null;
0 ignored issues
show
introduced by
The private property $section_name is not used, and could be removed.
Loading history...
226
227
    /**
228
     * @var array
229
     * @config
230
     */
231
    private static $casting = [
0 ignored issues
show
introduced by
The private property $casting is not used, and could be removed.
Loading history...
232
        'MainSvgIcon' => 'HTMLText'
233
    ];
234
235
    /**
236
     * The urls used for the links in the Help dropdown in the backend
237
     *
238
     * @config
239
     * @var array
240
     */
241
    private static $help_links = [
0 ignored issues
show
introduced by
The private property $help_links is not used, and could be removed.
Loading history...
242
        'CMS User help' => 'https://userhelp.silverstripe.org/en/4',
243
        'Developer docs' => 'https://docs.silverstripe.org/en/4/',
244
        'Community' => 'https://www.silverstripe.org/',
245
        'Feedback' => 'https://www.silverstripe.org/give-feedback/',
246
    ];
247
248
    /**
249
     * The href for the anchor on the Silverstripe logo
250
     *
251
     * @config
252
     * @var string
253
     */
254
    private static $application_link = '//www.silverstripe.org/';
0 ignored issues
show
introduced by
The private property $application_link is not used, and could be removed.
Loading history...
255
256
    /**
257
     * The application name
258
     *
259
     * @config
260
     * @var string
261
     */
262
    private static $application_name = 'Silverstripe';
0 ignored issues
show
introduced by
The private property $application_name is not used, and could be removed.
Loading history...
263
264
    /**
265
     * Current pageID for this request
266
     *
267
     * @var null
268
     */
269
    protected $pageID = null;
270
271
    /**
272
     * @var VersionProvider
273
     */
274
    protected $versionProvider;
275
276
    /**
277
     * @param Member $member
278
     * @return bool
279
     */
280
    public function canView($member = null)
281
    {
282
        if (!$member && $member !== false) {
0 ignored issues
show
introduced by
The condition $member !== false is always false.
Loading history...
283
            $member = Security::getCurrentUser();
284
        }
285
286
        // cms menus only for logged-in members
287
        if (!$member) {
288
            return false;
289
        }
290
291
        // alternative extended checks
292
        if ($this->hasMethod('alternateAccessCheck')) {
293
            $alternateAllowed = $this->alternateAccessCheck($member);
294
            if ($alternateAllowed === false) {
295
                return false;
296
            }
297
        }
298
299
        // Check for "CMS admin" permission
300
        if (Permission::checkMember($member, "CMS_ACCESS_LeftAndMain")) {
301
            return true;
302
        }
303
304
        // Check for LeftAndMain sub-class permissions
305
        $codes = $this->getRequiredPermissions();
306
        if ($codes === false) { // allow explicit FALSE to disable subclass check
307
            return true;
308
        }
309
        foreach ((array)$codes as $code) {
310
            if (!Permission::checkMember($member, $code)) {
311
                return false;
312
            }
313
        }
314
315
        return true;
316
    }
317
318
    /**
319
     * Get list of required permissions
320
     *
321
     * @return array|string|bool Code, array of codes, or false if no permission required
322
     */
323
    public static function getRequiredPermissions()
324
    {
325
        $class = get_called_class();
326
        // If the user is accessing LeftAndMain directly, only generic permissions are required.
327
        if ($class === self::class) {
328
            return 'CMS_ACCESS';
329
        }
330
        $code = Config::inst()->get($class, 'required_permission_codes');
331
        if ($code === false) {
332
            return false;
333
        }
334
        if ($code) {
335
            return $code;
336
        }
337
        return 'CMS_ACCESS_' . $class;
338
    }
339
340
    protected function includeGoogleFont()
341
    {
342
        $font = self::config()->google_font;
343
        if (!$font) {
344
            return;
345
        }
346
        $preconnect = <<<HTML
347
<link rel="preconnect" href="https://fonts.googleapis.com" />
348
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
349
HTML;
350
        Requirements::insertHeadTags($preconnect, "googlefontspreconnect");
351
        Requirements::css("https://fonts.googleapis.com/css2?$font");
352
    }
353
354
    protected function includeLastIcon()
355
    {
356
        $preconnect = <<<HTML
357
<link rel="preconnect" href="https://fonts.googleapis.com" />
358
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
359
HTML;
360
        Requirements::insertHeadTags($preconnect, "googlefontspreconnect");
361
362
        // Could also host locally https://marella.me/material-icons/demo/#two-tone
363
        Requirements::css("https://fonts.googleapis.com/icon?family=Material+Icons+Two+Tone");
364
        Requirements::javascript('lekoala/silverstripe-admini: client/js/last-icon.min.js', ["type" => "application/javascript"]);
365
        $nonce = CspProvider::getCspNonce();
366
        $lastIconScript = <<<JS
367
<script nonce="$nonce">
368
    window.LastIcon = {
369
            types: {
370
            material: "twotone",
371
            },
372
            defaultSet: "material",
373
            fonts: ["material"],
374
        };
375
</script>
376
JS;
377
        Requirements::insertHeadTags($lastIconScript, __FUNCTION__);
378
    }
379
380
    protected function includeFavicon()
381
    {
382
        $icon = $this->MainSvgIcon();
383
        $encodedIcon = str_replace(['"', '#'], ['%22', '%23'], $icon);
384
        $html = <<<HTML
385
<link
386
rel="icon"
387
type="image/svg+xml"
388
href="data:image/svg+xml,$encodedIcon"
389
/>
390
HTML;
391
        Requirements::insertHeadTags($html, __FUNCTION__);
392
    }
393
394
    /**
395
     * @return int
396
     */
397
    public function SessionKeepAlivePing()
398
    {
399
        return LeftAndMain::config()->uninherited('session_keepalive_ping');
400
    }
401
402
    /**
403
     * The icon to be used either as favicon or in the menu
404
     */
405
    public function MainSvgIcon(): string
406
    {
407
        $emoji = self::config()->svg_emoji ?? '💠';
408
        $icon = self::config()->svg_icon ?? '<svg xmlns="http://www.w3.org/2000/svg" width="256" height="256" viewBox="0 0 100 100"><text x="50%" y="52%" dominant-baseline="central" text-anchor="middle" font-size="120">' . $emoji . '</text></svg>';
409
        return $icon;
410
    }
411
412
    public function AdminDir(): string
413
    {
414
        $path = "js/admini.js";
415
        $resource = ModuleLoader::getModule('lekoala/silverstripe-base')->getResource($path);
416
        $dir = dirname($resource->getRelativePath());
417
        return $dir;
418
    }
419
420
    /**
421
     * Preload fonts
422
     */
423
    public function PreloadFonts()
424
    {
425
        $fonts = self::config()->preload_fonts;
426
        if (empty($fonts)) {
427
            return;
428
        }
429
        $dir = $this->AdminDir();
430
431
        $html = '';
432
        if (!empty($fonts)) {
433
            foreach ($fonts as $font) {
434
                $font = $dir . $font;
435
                // browsers will ignore preloaded fonts without the crossorigin attribute, which will cause the browser to actually fetch the font twice
436
                $html .= "<link rel=\"preload\" href=\"$font\" as=\"font\" type=\"font/woff2\" crossOrigin=\"anonymous\" >\n";
437
            }
438
        }
439
        Requirements::insertHeadTags($html, __FUNCTION__);
440
    }
441
442
    public function HasMinimenu(): bool
443
    {
444
        return (bool)Cookie::get("minimenu");
445
    }
446
447
    /**
448
     * In the CMS, we use tabs in the header
449
     * Do not render actual cms tabs
450
     *
451
     * @param Form $form
452
     * @return void
453
     */
454
    public function setCMSTabset(Form $form)
455
    {
456
        if ($form->Fields()->hasTabSet()) {
457
            $form->Fields()->findOrMakeTab('Root')->setTemplate('SilverStripe\\Forms\\CMSTabSet');
458
        }
459
    }
460
461
    /**
462
     * Check if the current request has a X-Formschema-Request header set.
463
     * Used by conditional logic that responds to validation results
464
     *
465
     * @return bool
466
     */
467
    protected function getSchemaRequested()
468
    {
469
        return false;
470
    }
471
472
    protected function loadExtraRequirements()
473
    {
474
        $extraJs = $this->config()->get('extra_requirements_javascript');
475
        if ($extraJs) {
476
            foreach ($extraJs as $file => $config) {
477
                if (is_numeric($file)) {
478
                    $file = $config;
479
                }
480
481
                Requirements::javascript($file);
482
            }
483
        }
484
485
        $extraCss = $this->config()->get('extra_requirements_css');
486
        if ($extraCss) {
487
            foreach ($extraCss as $file => $config) {
488
                if (is_numeric($file)) {
489
                    $file = $config;
490
                    $config = array();
491
                }
492
493
                Requirements::css($file, isset($config['media']) ? $config['media'] : null);
494
            }
495
        }
496
497
        $extraThemedCss = $this->config()->get('extra_requirements_themedCss');
498
        if ($extraThemedCss) {
499
            foreach ($extraThemedCss as $file => $config) {
500
                if (is_numeric($file)) {
501
                    $file = $config;
502
                    $config = array();
503
                }
504
505
                Requirements::themedCSS($file, isset($config['media']) ? $config['media'] : null);
506
            }
507
        }
508
    }
509
510
    /**
511
     * @uses LeftAndMainExtension->init()
512
     * @uses LeftAndMainExtension->accessedCMS()
513
     * @uses CMSMenu
514
     */
515
    protected function init()
516
    {
517
        parent::init();
518
519
        // SSViewer::config()->source_file_comments = true;
520
521
        // Replace GridField
522
        Injector::inst()->registerService(new TabulatorGrid(""), \SilverStripe\Forms\GridField\GridField::class);
523
524
        DeferBackend::config()->enable_js_modules = true;
525
        DeferBackend::replaceBackend();
526
527
        $this->showToasterMessage();
528
529
        HTTPCacheControlMiddleware::singleton()->disableCache();
530
531
        SSViewer::setRewriteHashLinksDefault(false);
532
        ContentNegotiator::setEnabled(false);
533
534
        // set language based on current user locale
535
        $member = Security::getCurrentUser();
536
        if (!empty($member->Locale)) {
537
            i18n::set_locale($member->Locale);
538
        }
539
540
        $response = $this->extendedCanView();
541
        if ($response) {
542
            return $response;
543
        }
544
545
        // Don't continue if there's already been a redirection request.
546
        if ($this->redirectedTo()) {
547
            //TODO: check why we return nothing?
548
            return;
549
        }
550
551
        // Audit logging hook
552
        if (empty($_REQUEST['executeForm']) && !$this->getRequest()->isAjax()) {
553
            $this->extend('accessedCMS');
554
        }
555
556
        $this->includeFavicon();
557
        $this->includeLastIcon();
558
        $this->includeGoogleFont();
559
560
        Requirements::javascript('lekoala/silverstripe-admini: client/js/admini.min.js');
561
        // This must be applied last, so we put it at the bottom manually because requirements backend may inject stuff in the body
562
        // Requirements::customScript("window.admini.init()");
563
        Requirements::css('lekoala/silverstripe-admini: client/css/admini.min.css');
564
        Requirements::css('lekoala/silverstripe-admini: client/css/custom.css');
565
566
        //TODO: restore these features
567
        // Requirements::add_i18n_javascript('silverstripe/admin:client/lang');
568
        // Requirements::add_i18n_javascript('silverstripe/admin:client/dist/moment-locales', false, false, true);
569
570
        // if (LeftAndMain::config()->uninherited('session_keepalive_ping')) {
571
        //     Requirements::javascript('silverstripe/admin: client/dist/js/LeftAndMain.Ping.js');
572
        // }
573
574
        $this->loadExtraRequirements();
575
        $this->extend('init');
576
577
        // Assign default cms theme and replace user-specified themes
578
        // This allows us for instance to set custom form templates for BS5
579
        SSViewer::set_themes(LeftAndMain::config()->uninherited('admin_themes'));
580
581
        // Versioned support
582
        if (class_exists(Versioned::class)) {
583
            // Set the current reading mode
584
            Versioned::set_stage(Versioned::DRAFT);
585
586
            // Set default reading mode to suppress ?stage=Stage querystring params in CMS
587
            Versioned::set_default_reading_mode(Versioned::get_reading_mode());
588
        }
589
    }
590
591
    /**
592
     * Allow customisation of the access check by a extension
593
     * Also all the canView() check to execute Controller::redirect()
594
     * @return HTTPResponse|null
595
     */
596
    protected function extendedCanView()
597
    {
598
        if ($this->canView() || $this->getResponse()->isFinished()) {
599
            return null;
600
        }
601
602
        // When access /admin/, we should try a redirect to another part of the admin rather than be locked out
603
        $menu = $this->MainMenu();
604
        foreach ($menu as $candidate) {
605
            $canView = $candidate->Link &&
606
                $candidate->Link != $this->Link()
607
                && $candidate->MenuItem->controller
608
                && singleton($candidate->MenuItem->controller)->canView();
609
            if ($canView) {
610
                $this->redirect($candidate->Link);
611
                return;
612
            }
613
        }
614
615
        if (Security::getCurrentUser()) {
616
            $this->getRequest()->getSession()->clear("BackURL");
617
        }
618
619
        // if no alternate menu items have matched, return a permission error
620
        $messageSet = array(
621
            'default' => _t(
622
                __CLASS__ . '.PERMDEFAULT',
623
                "You must be logged in to access the administration area; please enter your credentials below."
624
            ),
625
            'alreadyLoggedIn' => _t(
626
                __CLASS__ . '.PERMALREADY',
627
                "I'm sorry, but you can't access that part of the CMS.  If you want to log in as someone else, do"
628
                    . " so below."
629
            ),
630
            'logInAgain' => _t(
631
                __CLASS__ . '.PERMAGAIN',
632
                "You have been logged out of the CMS.  If you would like to log in again, enter a username and"
633
                    . " password below."
634
            ),
635
        );
636
637
        return Security::permissionFailure($this, $messageSet);
638
    }
639
640
    public function handleRequest(HTTPRequest $request)
641
    {
642
        try {
643
            $response = parent::handleRequest($request);
644
        } catch (ValidationException $e) {
645
            // Nicer presentation of model-level validation errors
646
            $msgs = _t(__CLASS__ . '.ValidationError', 'Validation error') . ': '
647
                . $e->getMessage();
648
            $this->sessionMessage($msgs, "bad");
649
            return $this->redirectBack();
650
        }
651
652
        $title = $this->Title();
653
654
        //TODO: check this when implementing ajax
655
        if (!$response->getHeader('X-Controller')) {
656
            $response->addHeader('X-Controller', static::class);
657
        }
658
        if (!$response->getHeader('X-Title')) {
659
            $response->addHeader('X-Title', urlencode($title));
660
        }
661
662
        // Prevent clickjacking, see https://developer.mozilla.org/en-US/docs/HTTP/X-Frame-Options
663
        $originalResponse = $this->getResponse();
664
        $originalResponse->addHeader('X-Frame-Options', LeftAndMain::config()->uninherited('frame_options'));
665
        $originalResponse->addHeader('Vary', 'X-Requested-With');
666
667
        return $response;
668
    }
669
670
    /**
671
     * Overloaded redirection logic to trigger a fake redirect on ajax requests.
672
     * While this violates HTTP principles, its the only way to work around the
673
     * fact that browsers handle HTTP redirects opaquely, no intervention via JS is possible.
674
     * In isolation, that's not a problem - but combined with history.pushState()
675
     * it means we would request the same redirection URL twice if we want to update the URL as well.
676
     * See LeftAndMain.js for the required jQuery ajaxComplete handlers.
677
     *
678
     * @param string $url
679
     * @param int $code
680
     * @return HTTPResponse|string
681
     */
682
    public function redirect($url, $code = 302)
683
    {
684
        //TODO: check this when implementing ajax navigation
685
        if ($this->getRequest()->isAjax()) {
686
            $response = $this->getResponse();
687
            $response->addHeader('X-ControllerURL', $url);
688
            if ($this->getRequest()->getHeader('X-Pjax') && !$response->getHeader('X-Pjax')) {
689
                $response->addHeader('X-Pjax', $this->getRequest()->getHeader('X-Pjax'));
690
            }
691
            $newResponse = new HTTPResponse(
692
                $response->getBody(),
693
                $response->getStatusCode(),
694
                $response->getStatusDescription()
695
            );
696
            foreach ($response->getHeaders() as $k => $v) {
697
                $newResponse->addHeader($k, $v);
698
            }
699
700
            // $newResponse->setIsFinished(true);
701
            // $this->setResponse($newResponse);
702
703
            return ''; // Actual response will be re-requested by client
704
        } else {
705
            return parent::redirect($url, $code);
706
        }
707
    }
708
709
    /**
710
     * @param HTTPRequest $request
711
     * @return HTTPResponse|DBHTMLText
712
     */
713
    public function index($request)
0 ignored issues
show
Unused Code introduced by
The parameter $request is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

713
    public function index(/** @scrutinizer ignore-unused */ $request)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
714
    {
715
        return $this->renderWith($this->getViewer('show'));
716
    }
717
718
    /**
719
     * You should implement a Link() function in your subclass of LeftAndMain,
720
     * to point to the URL of that particular controller.
721
     *
722
     * @param string $action
723
     * @return string
724
     */
725
    public function Link($action = null)
726
    {
727
        // LeftAndMain methods have a top-level uri access
728
        if (static::class === LeftAndMain::class) {
0 ignored issues
show
introduced by
The condition static::class === LeKoal...mini\LeftAndMain::class is always true.
Loading history...
729
            $segment = '';
730
        } else {
731
            // Get url_segment
732
            $segment = $this->config()->get('url_segment');
733
            if (!$segment) {
734
                throw new BadMethodCallException(
735
                    sprintf('LeftAndMain subclasses (%s) must have url_segment', static::class)
736
                );
737
            }
738
        }
739
740
        $link = Controller::join_links(
741
            AdminiRootController::admin_url(),
742
            $segment,
743
            '/', // trailing slash needed if $action is null!
744
            "$action"
745
        );
746
        $this->extend('updateLink', $link);
747
        return $link;
748
    }
749
750
    /**
751
     * Get menu title for this section (translated)
752
     *
753
     * @param string $class Optional class name if called on LeftAndMain directly
754
     * @param bool $localise Determine if menu title should be localised via i18n.
755
     * @return string Menu title for the given class
756
     */
757
    public static function menu_title($class = null, $localise = true)
758
    {
759
        if ($class && is_subclass_of($class, __CLASS__)) {
760
            // Respect oveloading of menu_title() in subclasses
761
            return $class::menu_title(null, $localise);
762
        }
763
        if (!$class) {
764
            $class = get_called_class();
765
        }
766
767
        // Get default class title
768
        $title = static::config()->get('menu_title');
769
        if (!$title) {
770
            $title = preg_replace('/Admin$/', '', $class);
771
        }
772
773
        // Check localisation
774
        if (!$localise) {
775
            return $title;
776
        }
777
        return i18n::_t("{$class}.MENUTITLE", $title);
778
    }
779
780
    /**
781
     * Return the name for the menu icon
782
     * @param string $class
783
     * @return string
784
     */
785
    public static function menu_icon_for_class($class)
786
    {
787
        return Config::inst()->get($class, 'menu_icon');
788
    }
789
790
    /**
791
     * @param HTTPRequest $request
792
     * @return HTTPResponse|DBHTMLText
793
     * @throws HTTPResponse_Exception
794
     */
795
    public function show($request)
0 ignored issues
show
Unused Code introduced by
The parameter $request is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

795
    public function show(/** @scrutinizer ignore-unused */ $request)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
796
    {
797
        // TODO Necessary for TableListField URLs to work properly
798
        // TODO: check why this is needed
799
        // if ($request->param('ID')) {
800
        //     $this->setCurrentPageID($request->param('ID'));
801
        // }
802
        return $this->renderWith($this->getViewer('show'));
803
    }
804
805
    //------------------------------------------------------------------------------------------//
806
    // Main UI components
807
808
    /**
809
     * Returns the main menu of the CMS.  This is also used by init()
810
     * to work out which sections the user has access to.
811
     *
812
     * @param bool $cached
813
     * @return SS_List
814
     */
815
    public function MainMenu($cached = true)
816
    {
817
        static $menuCache = null;
818
        if ($menuCache === null || !$cached) {
819
            // Don't accidentally return a menu if you're not logged in - it's used to determine access.
820
            if (!Security::getCurrentUser()) {
821
                return new ArrayList();
822
            }
823
824
            // Encode into DO set
825
            $menu = new ArrayList();
826
            $menuItems = CMSMenu::get_viewable_menu_items();
827
828
            // extra styling for custom menu-icons
829
            $menuIconStyling = '';
830
831
            if ($menuItems) {
832
                /** @var CMSMenuItem $menuItem */
833
                foreach ($menuItems as $code => $menuItem) {
834
                    // alternate permission checks (in addition to LeftAndMain->canView())
835
                    $alternateCheck = isset($menuItem->controller)
836
                        && $this->hasMethod('alternateMenuDisplayCheck')
837
                        && !$this->alternateMenuDisplayCheck($menuItem->controller);
838
                    if ($alternateCheck) {
839
                        continue;
840
                    }
841
842
                    // linking mode
843
                    $linkingmode = "link";
844
                    if ($menuItem->controller && get_class($this) == $menuItem->controller) {
845
                        $linkingmode = "current";
846
                    } elseif (strpos($this->Link(), $menuItem->url) !== false) {
847
                        if ($this->Link() == $menuItem->url) {
848
                            $linkingmode = "current";
849
850
                            // default menu is the one with a blank {@link url_segment}
851
                        } elseif (singleton($menuItem->controller)->config()->get('url_segment') == '') {
852
                            if ($this->Link() == AdminiRootController::admin_url()) {
853
                                $linkingmode = "current";
854
                            }
855
                        } else {
856
                            $linkingmode = "current";
857
                        }
858
                    }
859
860
                    // already set in CMSMenu::populate_menu(), but from a static pre-controller
861
                    // context, so doesn't respect the current user locale in _t() calls - as a workaround,
862
                    // we simply call LeftAndMain::menu_title() again
863
                    // if we're dealing with a controller
864
                    if ($menuItem->controller) {
865
                        $title = LeftAndMain::menu_title($menuItem->controller);
866
                    } else {
867
                        $title = $menuItem->title;
868
                    }
869
870
                    // Provide styling for custom $menu-icon. Done here instead of in
871
                    // CMSMenu::populate_menu(), because the icon is part of
872
                    // the CMS right pane for the specified class as well...
873
                    $IconName = '';
874
                    if ($menuItem->controller) {
875
                        $IconName = LeftAndMain::menu_icon_for_class($menuItem->controller);
876
                    } else {
877
                        $IconName = $menuItem->iconName;
878
                    }
879
                    if (!$IconName) {
880
                        $IconName = "arrow_right";
881
                    }
882
                    $menuItem->addExtraClass("sidebar-link");
883
884
                    $menu->push(new ArrayData([
885
                        "MenuItem" => $menuItem,
886
                        "AttributesHTML" => $menuItem->getAttributesHTML(),
887
                        "Title" => $title,
888
                        "Code" => $code,
889
                        "IconName" => $IconName,
890
                        "Link" => $menuItem->url,
891
                        "LinkingMode" => $linkingmode
892
                    ]));
893
                }
894
            }
895
            if ($menuIconStyling) {
896
                Requirements::customCSS($menuIconStyling);
897
            }
898
899
            $menuCache = $menu;
900
        }
901
902
        return $menuCache;
903
    }
904
905
    public function Menu()
906
    {
907
        return $this->renderWith($this->getTemplatesWithSuffix('_Menu'));
908
    }
909
910
    /**
911
     * @todo Wrap in CMSMenu instance accessor
912
     * @return ArrayData A single menu entry (see {@link MainMenu})
913
     */
914
    public function MenuCurrentItem()
915
    {
916
        $items = $this->MainMenu();
917
        return $items->find('LinkingMode', 'current');
918
    }
919
920
    /**
921
     * Return appropriate template(s) for this class, with the given suffix using
922
     * {@link SSViewer::get_templates_by_class()}
923
     *
924
     * @param string $suffix
925
     * @return string|array
926
     */
927
    public function getTemplatesWithSuffix($suffix)
928
    {
929
        $templates = SSViewer::get_templates_by_class(get_class($this), $suffix, __CLASS__);
930
        return SSViewer::chooseTemplate($templates);
931
    }
932
933
    public function Content()
934
    {
935
        return $this->renderWith($this->getTemplatesWithSuffix('_Content'));
936
    }
937
938
    /**
939
     * Get dataobject from the current ID
940
     *
941
     * @param int|DataObject $id ID or object
942
     * @return DataObject
943
     */
944
    public function getRecord($id)
945
    {
946
        $className = $this->config()->get('tree_class');
947
        if (!$className) {
948
            return null;
949
        }
950
        if ($id instanceof $className) {
951
            /** @var DataObject $id */
952
            return $id;
953
        }
954
        if ($id === 'root') {
0 ignored issues
show
introduced by
The condition $id === 'root' is always false.
Loading history...
955
            return DataObject::singleton($className);
956
        }
957
        if (is_numeric($id)) {
958
            return DataObject::get_by_id($className, $id);
959
        }
960
        return null;
961
    }
962
963
    /**
964
     * Called by CMSBreadcrumbs.ss
965
     * @param bool $unlinked
966
     * @return ArrayList
967
     */
968
    public function Breadcrumbs($unlinked = false)
969
    {
970
        $items = new ArrayList(array(
971
            new ArrayData(array(
972
                'Title' => $this->menu_title(),
973
                'Link' => ($unlinked) ? false : $this->Link()
974
            ))
975
        ));
976
977
        return $items;
978
    }
979
980
    /**
981
     * Save  handler
982
     *
983
     * @param array $data
984
     * @param Form $form
985
     * @return HTTPResponse
986
     */
987
    public function save($data, $form)
988
    {
989
        $request = $this->getRequest();
0 ignored issues
show
Unused Code introduced by
The assignment to $request is dead and can be removed.
Loading history...
990
        $className = $this->config()->get('tree_class');
991
992
        // Existing or new record?
993
        $id = $data['ID'];
994
        if (is_numeric($id) && $id > 0) {
995
            $record = DataObject::get_by_id($className, $id);
996
            if ($record && !$record->canEdit()) {
997
                return Security::permissionFailure($this);
998
            }
999
            if (!$record || !$record->ID) {
0 ignored issues
show
introduced by
$record is of type SilverStripe\ORM\DataObject, thus it always evaluated to true.
Loading history...
1000
                $this->httpError(404, "Bad record ID #" . (int)$id);
1001
            }
1002
        } else {
1003
            if (!singleton($this->config()->get('tree_class'))->canCreate()) {
1004
                return Security::permissionFailure($this);
1005
            }
1006
            $record = $this->getNewItem($id, false);
1007
        }
1008
1009
        // save form data into record
1010
        $form->saveInto($record, true);
0 ignored issues
show
Bug introduced by
true of type true is incompatible with the type SilverStripe\Forms\FieldList expected by parameter $fieldList of SilverStripe\Forms\Form::saveInto(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1010
        $form->saveInto($record, /** @scrutinizer ignore-type */ true);
Loading history...
1011
        $record->write();
1012
        $this->extend('onAfterSave', $record);
1013
1014
        //TODO: investigate if this is needed
1015
        // $this->setCurrentPageID($record->ID);
1016
1017
        $message = _t(__CLASS__ . '.SAVEDUP', 'Saved.');
1018
        $this->sessionMessage($message, "good");
1019
        return $this->redirectBack();
1020
    }
1021
1022
    /**
1023
     * Create new item.
1024
     *
1025
     * @param string|int $id
1026
     * @param bool $setID
1027
     * @return DataObject
1028
     */
1029
    public function getNewItem($id, $setID = true)
1030
    {
1031
        $class = $this->config()->get('tree_class');
1032
        $object = Injector::inst()->create($class);
1033
        if ($setID) {
1034
            $object->ID = $id;
1035
        }
1036
        return $object;
1037
    }
1038
1039
    public function delete($data, $form)
0 ignored issues
show
Unused Code introduced by
The parameter $form is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

1039
    public function delete($data, /** @scrutinizer ignore-unused */ $form)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
1040
    {
1041
        $className = $this->config()->get('tree_class');
1042
1043
        $id = $data['ID'];
1044
        $record = DataObject::get_by_id($className, $id);
1045
        if ($record && !$record->canDelete()) {
1046
            return Security::permissionFailure();
1047
        }
1048
        if (!$record || !$record->ID) {
0 ignored issues
show
introduced by
$record is of type SilverStripe\ORM\DataObject, thus it always evaluated to true.
Loading history...
1049
            $this->httpError(404, "Bad record ID #" . (int)$id);
1050
        }
1051
1052
        $record->delete();
1053
        $this->sessionMessage(_t(__CLASS__ . '.DELETED', 'Deleted.'));
1054
        return $this->redirectBack();
1055
    }
1056
1057
    /**
1058
     * Retrieves an edit form, either for display, or to process submitted data.
1059
     * Also used in the template rendered through {@link Right()} in the $EditForm placeholder.
1060
     *
1061
     * This is a "pseudo-abstract" methoed, usually connected to a {@link getEditForm()}
1062
     * method in an entwine subclass. This method can accept a record identifier,
1063
     * selected either in custom logic, or through {@link currentPageID()}.
1064
     * The form usually construct itself from {@link DataObject->getCMSFields()}
1065
     * for the specific managed subclass defined in {@link LeftAndMain::$tree_class}.
1066
     *
1067
     * @param HTTPRequest $request Passed if executing a HTTPRequest directly on the form.
1068
     * If empty, this is invoked as $EditForm in the template
1069
     * @return Form Should return a form regardless wether a record has been found.
1070
     *  Form might be readonly if the current user doesn't have the permission to edit
1071
     *  the record.
1072
     */
1073
    public function EditForm($request = null)
0 ignored issues
show
Unused Code introduced by
The parameter $request is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

1073
    public function EditForm(/** @scrutinizer ignore-unused */ $request = null)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
1074
    {
1075
        return $this->getEditForm();
1076
    }
1077
1078
    /**
1079
     * Calls {@link SiteTree->getCMSFields()} by default to determine the form fields to display.
1080
     *
1081
     * @param int $id
1082
     * @param FieldList $fields
1083
     * @return Form
1084
     */
1085
    public function getEditForm($id = null, $fields = null)
1086
    {
1087
        if ($id === null || $id === 0) {
1088
            $id = $this->currentPageID();
1089
        }
1090
1091
        // Check record exists
1092
        $record = $this->getRecord($id);
1093
        if (!$record) {
0 ignored issues
show
introduced by
$record is of type SilverStripe\ORM\DataObject, thus it always evaluated to true.
Loading history...
1094
            return null;
1095
        }
1096
1097
        // Check if this record is viewable
1098
        if ($record && !$record->canView()) {
1099
            $response = Security::permissionFailure($this);
1100
            $this->setResponse($response);
1101
            return null;
1102
        }
1103
1104
        $fields = $fields ?: $record->getCMSFields();
1105
        if (!$fields) {
0 ignored issues
show
introduced by
$fields is of type SilverStripe\Forms\FieldList, thus it always evaluated to true.
Loading history...
1106
            throw new LogicException(
1107
                "getCMSFields() returned null  - it should return a FieldList object.
1108
                Perhaps you forgot to put a return statement at the end of your method?"
1109
            );
1110
        }
1111
1112
        // Add hidden fields which are required for saving the record
1113
        // and loading the UI state
1114
        if (!$fields->dataFieldByName('ClassName')) {
1115
            $fields->push(new HiddenField('ClassName'));
1116
        }
1117
1118
        $tree_class = $this->config()->get('tree_class');
1119
        $needParentID = $tree_class::has_extension(Hierarchy::class) && !$fields->dataFieldByName('ParentID');
1120
        if ($needParentID) {
1121
            $fields->push(new HiddenField('ParentID'));
1122
        }
1123
1124
        if ($record->hasMethod('getAdminiActions')) {
1125
            $actions = $record->getAdminiActions();
1126
        } else {
1127
            $actions = $record->getCMSActions();
1128
            // add default actions if none are defined
1129
            if (!$actions || !$actions->count()) {
0 ignored issues
show
introduced by
$actions is of type SilverStripe\Forms\FieldList, thus it always evaluated to true.
Loading history...
1130
                if ($record->hasMethod('canEdit') && $record->canEdit()) {
1131
                    $actions->push(
1132
                        FormAction::create('save', _t('LeKoala\\Admini\\LeftAndMain.SAVE', 'Save'))
1133
                            ->addExtraClass('btn btn-outline-primary')
1134
                            ->setIcon(MaterialIcons::DONE)
1135
                    );
1136
                }
1137
                if ($record->hasMethod('canDelete') && $record->canDelete()) {
1138
                    $actions->push(
1139
                        FormAction::create('delete', _t('LeKoala\\Admini\\LeftAndMain.DELETE', 'Delete'))
1140
                            ->addExtraClass('btn btn-danger')
1141
                            ->setIcon(MaterialIcons::DELETE)
1142
                    );
1143
                }
1144
            }
1145
        }
1146
1147
        $form = Form::create(
1148
            $this,
1149
            "EditForm",
1150
            $fields,
1151
            $actions
1152
        )->setHTMLID('Form_EditForm');
1153
        $form->addExtraClass('cms-edit-form needs-validation');
1154
        $form->loadDataFrom($record);
1155
        $form->setTemplate($this->getTemplatesWithSuffix('_EditForm'));
1156
        $form->setAttribute('data-validation-message', _t('LeKoala\\Admini\\LeftAndMain.FIELDS_INVALID', 'Some fields are invalid'));
1157
1158
        //TODO: check if this is needed
1159
        $form->setRequestHandler(LeftAndMainFormRequestHandler::create($form));
1160
1161
        $validator = $record->getCMSCompositeValidator();
1162
        if ($validator) {
0 ignored issues
show
introduced by
$validator is of type SilverStripe\Forms\CompositeValidator, thus it always evaluated to true.
Loading history...
1163
            $form->setValidator($validator);
1164
        } else {
1165
            $form->unsetValidator();
1166
        }
1167
1168
        // Check if this form is readonly
1169
        if (!$record->canEdit()) {
1170
            $readonlyFields = $form->Fields()->makeReadonly();
1171
            $form->setFields($readonlyFields);
1172
        }
1173
        return $form;
1174
    }
1175
1176
    /**
1177
     * Returns a placeholder form, used by {@link getEditForm()} if no record is selected.
1178
     * Our javascript logic always requires a form to be present in the CMS interface.
1179
     *
1180
     * @return Form
1181
     */
1182
    public function EmptyForm()
1183
    {
1184
        //TODO: check if this is needed. It's not very elegant
1185
        $form = Form::create(
1186
            $this,
1187
            "EditForm",
1188
            new FieldList(),
1189
            new FieldList()
1190
        )->setHTMLID('Form_EditForm');
1191
        $form->unsetValidator();
1192
        $form->addExtraClass('cms-edit-form');
1193
        $form->addExtraClass('root-form');
1194
        $form->setTemplate($this->getTemplatesWithSuffix('_EditForm'));
1195
        $form->setAttribute('data-pjax-fragment', 'CurrentForm');
1196
1197
        return $form;
1198
    }
1199
1200
    /**
1201
     * Renders a panel containing tools which apply to all displayed
1202
     * "content" (mostly through {@link EditForm()}), for example a tree navigation or a filter panel.
1203
     * Auto-detects applicable templates by naming convention: "<controller classname>_Tools.ss",
1204
     * and takes the most specific template (see {@link getTemplatesWithSuffix()}).
1205
     * To explicitly disable the panel in the subclass, simply create a more specific, empty template.
1206
     *
1207
     * @return string|bool HTML
1208
     */
1209
    public function Tools()
1210
    {
1211
        $templates = $this->getTemplatesWithSuffix('_Tools');
1212
        if ($templates) {
1213
            $viewer = SSViewer::create($templates);
1214
            return $viewer->process($this);
1215
        }
1216
        return false;
1217
    }
1218
1219
    /**
1220
     * Renders a panel containing tools which apply to the currently displayed edit form.
1221
     * The main difference to {@link Tools()} is that the panel is displayed within
1222
     * the element structure of the form panel (rendered through {@link EditForm}).
1223
     * This means the panel will be loaded alongside new forms, and refreshed upon save,
1224
     * which can mean a performance hit, depending on how complex your panel logic gets.
1225
     * Any form fields contained in the returned markup will also be submitted with the main form,
1226
     * which might be desired depending on the implementation details.
1227
     *
1228
     * @return string|bool HTML
1229
     */
1230
    public function EditFormTools()
1231
    {
1232
        $templates = $this->getTemplatesWithSuffix('_EditFormTools');
1233
        if ($templates) {
1234
            $viewer = SSViewer::create($templates);
1235
            return $viewer->process($this);
1236
        }
1237
        return false;
1238
    }
1239
1240
    /**
1241
     * @return Form|bool|array
1242
     */
1243
    public function printable()
1244
    {
1245
        $form = $this->getEditForm($this->currentPageID());
1246
        if (!$form) {
0 ignored issues
show
introduced by
$form is of type SilverStripe\Forms\Form, thus it always evaluated to true.
Loading history...
1247
            return false;
1248
        }
1249
1250
        $form->transform(new PrintableTransformation());
1251
        $form->setActions(null);
1252
1253
        Requirements::clear();
1254
1255
        // TODO: check admini print styles ?
1256
        // Requirements::css('silverstripe/admin: dist/css/LeftAndMain_printable.css');
1257
1258
        return array(
1259
            "PrintForm" => $form
1260
        );
1261
    }
1262
1263
    /**
1264
     * Identifier for the currently shown record,
1265
     * in most cases a database ID. Inspects the following
1266
     * sources (in this order):
1267
     * - GET/POST parameter named 'ID'
1268
     * - URL parameter named 'ID'
1269
     * - Session value namespaced by classname, e.g. "CMSMain.currentPage"
1270
     *
1271
     * @return int
1272
     */
1273
    public function currentPageID()
1274
    {
1275
        if ($this->pageID) {
1276
            return $this->pageID;
1277
        }
1278
        if ($this->getRequest()->requestVar('ID') && is_numeric($this->getRequest()->requestVar('ID'))) {
1279
            return $this->getRequest()->requestVar('ID');
1280
        }
1281
1282
        if ($this->getRequest()->requestVar('CMSMainCurrentPageID') && is_numeric($this->getRequest()->requestVar('CMSMainCurrentPageID'))) {
1283
            // see GridFieldDetailForm::ItemEditForm
1284
            return $this->getRequest()->requestVar('CMSMainCurrentPageID');
1285
        }
1286
1287
        if (isset($this->urlParams['ID']) && is_numeric($this->urlParams['ID'])) {
1288
            return $this->urlParams['ID'];
1289
        }
1290
1291
        if (is_numeric($this->getRequest()->param('ID'))) {
1292
            return (int)$this->getRequest()->param('ID');
1293
        }
1294
1295
        /** @deprecated */
1296
        //TODO: check if we can remove this altogether
1297
        $session = $this->getRequest()->getSession();
1298
        return $session->get($this->sessionNamespace() . ".currentPage") ?: null;
1299
    }
1300
1301
    /**
1302
     * Uses {@link getRecord()} and {@link currentPageID()}
1303
     * to get the currently selected record.
1304
     *
1305
     * @return DataObject
1306
     */
1307
    public function currentPage()
1308
    {
1309
        return $this->getRecord($this->currentPageID());
1310
    }
1311
1312
    /**
1313
     * Compares a given record to the currently selected one (if any).
1314
     * Used for marking the current tree node.
1315
     *
1316
     * @param DataObject $record
1317
     * @return bool
1318
     */
1319
    public function isCurrentPage(DataObject $record)
1320
    {
1321
        return ($record->ID == $this->currentPageID());
1322
    }
1323
1324
    /**
1325
     * @return string
1326
     */
1327
    protected function sessionNamespace()
1328
    {
1329
        $override = $this->config()->get('session_namespace');
1330
        return $override ? $override : static::class;
1331
    }
1332
1333
    /**
1334
     * Return the version number of this application, ie. 'CMS: 4.2.1'
1335
     *
1336
     * @return string
1337
     */
1338
    public function CMSVersion()
1339
    {
1340
        return $this->getVersionProvider()->getVersion();
1341
    }
1342
1343
    /**
1344
     * Return the version number of the CMS in the 'major.minor' format, e.g. '4.2'
1345
     * Will handle 4.10.x-dev by removing .x-dev
1346
     *
1347
     * @return string
1348
     */
1349
    public function CMSVersionNumber()
1350
    {
1351
        $moduleName = array_keys($this->getVersionProvider()->getModules())[0];
1352
        $lockModules = $this->getVersionProvider()->getModuleVersionFromComposer([$moduleName]);
1353
        if (!isset($lockModules[$moduleName])) {
1354
            return '';
1355
        }
1356
        $version = $lockModules[$moduleName];
1357
        if (preg_match('#^([0-9]+)\.([0-9]+)#', $version, $m)) {
1358
            return $m[1] . '.' . $m[2];
1359
        }
1360
        return $version;
1361
    }
1362
1363
    /**
1364
     * @return SiteConfig
1365
     */
1366
    public function SiteConfig()
1367
    {
1368
        return class_exists(SiteConfig::class) ? SiteConfig::current_site_config() : null;
1369
    }
1370
1371
    /**
1372
     * Returns help_links in a format readable by a template
1373
     * @return ArrayList
1374
     */
1375
    public function getHelpLinks()
1376
    {
1377
        $helpLinks = $this->config()->get('help_links');
1378
        $formattedLinks = [];
1379
1380
        $helpLink = $this->config()->get('help_link');
1381
        if ($helpLink) {
1382
            Deprecation::notice('5.0', 'Use $help_links instead of $help_link');
1383
            $helpLinks['CMS User help'] = $helpLink;
1384
        }
1385
1386
        foreach ($helpLinks as $key => $value) {
1387
            $translationKey = str_replace(' ', '', $key);
1388
1389
            $formattedLinks[] = [
1390
                'Title' => _t(__CLASS__ . '.' . $translationKey, $key),
1391
                'URL' => $value
1392
            ];
1393
        }
1394
1395
        return ArrayList::create($formattedLinks);
1396
    }
1397
1398
    /**
1399
     * @return string
1400
     */
1401
    public function ApplicationLink()
1402
    {
1403
        return $this->config()->get('application_link');
1404
    }
1405
1406
    /**
1407
     * Get the application name.
1408
     *
1409
     * @return string
1410
     */
1411
    public function getApplicationName()
1412
    {
1413
        return $this->config()->get('application_name');
1414
    }
1415
1416
    /**
1417
     * @return string
1418
     */
1419
    public function Title()
1420
    {
1421
        $app = $this->getApplicationName();
1422
        return ($section = $this->SectionTitle()) ? sprintf('%s - %s', $app, $section) : $app;
1423
    }
1424
1425
    /**
1426
     * Return the title of the current section. Either this is pulled from
1427
     * the current panel's menu_title or from the first active menu
1428
     *
1429
     * @return string
1430
     */
1431
    public function SectionTitle()
1432
    {
1433
        $title = $this->menu_title();
1434
        if ($title) {
1435
            return $title;
1436
        }
1437
1438
        foreach ($this->MainMenu() as $menuItem) {
1439
            if ($menuItem->LinkingMode != 'link') {
1440
                return $menuItem->Title;
1441
            }
1442
        }
1443
        return null;
1444
    }
1445
1446
    /**
1447
     * Generate a logout url with BackURL to the CMS
1448
     *
1449
     * @return string
1450
     */
1451
    public function LogoutURL()
1452
    {
1453
        return Controller::join_links(Security::logout_url(), '?' . http_build_query([
1454
            'BackURL' => AdminiRootController::admin_url(),
1455
        ]));
1456
    }
1457
1458
    /**
1459
     * Same as {@link ViewableData->CSSClasses()}, but with a changed name
1460
     * to avoid problems when using {@link ViewableData->customise()}
1461
     * (which always returns "ArrayData" from the $original object).
1462
     *
1463
     * @return string
1464
     */
1465
    public function BaseCSSClasses()
1466
    {
1467
        return $this->CSSClasses(Controller::class);
1468
    }
1469
1470
    /**
1471
     * @return string
1472
     */
1473
    public function Locale()
1474
    {
1475
        return DBField::create_field('Locale', i18n::get_locale());
1476
    }
1477
1478
    public function providePermissions()
1479
    {
1480
        $perms = array(
1481
            "CMS_ACCESS_LeftAndMain" => array(
1482
                'name' => _t(__CLASS__ . '.ACCESSALLINTERFACES', 'Access to all CMS sections'),
1483
                'category' => _t('SilverStripe\\Security\\Permission.CMS_ACCESS_CATEGORY', 'CMS Access'),
1484
                'help' => _t(__CLASS__ . '.ACCESSALLINTERFACESHELP', 'Overrules more specific access settings.'),
1485
                'sort' => -100
1486
            )
1487
        );
1488
1489
        // Add any custom ModelAdmin subclasses. Can't put this on ModelAdmin itself
1490
        // since its marked abstract, and needs to be singleton instanciated.
1491
        foreach (ClassInfo::subclassesFor(ModelAdmin::class) as $i => $class) {
1492
            if ($class === ModelAdmin::class) {
1493
                continue;
1494
            }
1495
            if (ClassInfo::classImplements($class, TestOnly::class)) {
1496
                continue;
1497
            }
1498
1499
            // Check if modeladmin has explicit required_permission_codes option.
1500
            // If a modeladmin is namespaced you can apply this config to override
1501
            // the default permission generation based on fully qualified class name.
1502
            $code = $class::getRequiredPermissions();
1503
1504
            if (!$code) {
1505
                continue;
1506
            }
1507
            // Get first permission if multiple specified
1508
            if (is_array($code)) {
1509
                $code = reset($code);
1510
            }
1511
            $title = LeftAndMain::menu_title($class);
1512
            $perms[$code] = array(
1513
                'name' => _t(
1514
                    'LeKoala\\Admini\\LeftAndMain.ACCESS',
1515
                    "Access to '{title}' section",
1516
                    "Item in permission selection identifying the admin section. Example: Access to 'Files & Images'",
1517
                    array('title' => $title)
1518
                ),
1519
                'category' => _t('SilverStripe\\Security\\Permission.CMS_ACCESS_CATEGORY', 'CMS Access')
1520
            );
1521
        }
1522
1523
        return $perms;
1524
    }
1525
1526
    /**
1527
     * Set the SilverStripe version provider to use
1528
     *
1529
     * @param VersionProvider $provider
1530
     * @return $this
1531
     */
1532
    public function setVersionProvider(VersionProvider $provider)
1533
    {
1534
        $this->versionProvider = $provider;
1535
        return $this;
1536
    }
1537
1538
    /**
1539
     * Get the SilverStripe version provider
1540
     *
1541
     * @return VersionProvider
1542
     */
1543
    public function getVersionProvider()
1544
    {
1545
        if (!$this->versionProvider) {
1546
            $this->versionProvider = new VersionProvider();
1547
        }
1548
        return $this->versionProvider;
1549
    }
1550
}
1551