Passed
Push — master ( db3cc9...9bdeed )
by Thomas
11:48
created

LeftAndMain::UseBootstrap5()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

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

745
    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...
746
    {
747
        return $this->renderWith($this->getViewer('show'));
748
    }
749
750
    /**
751
     * You should implement a Link() function in your subclass of LeftAndMain,
752
     * to point to the URL of that particular controller.
753
     *
754
     * @param string $action
755
     * @return string
756
     */
757
    public function Link($action = null)
758
    {
759
        // LeftAndMain methods have a top-level uri access
760
        if (static::class === LeftAndMain::class) {
0 ignored issues
show
introduced by
The condition static::class === LeKoal...mini\LeftAndMain::class is always true.
Loading history...
761
            $segment = '';
762
        } else {
763
            // Get url_segment
764
            $segment = $this->config()->get('url_segment');
765
            if (!$segment) {
766
                throw new BadMethodCallException(
767
                    sprintf('LeftAndMain subclasses (%s) must have url_segment', static::class)
768
                );
769
            }
770
        }
771
772
        $link = Controller::join_links(
773
            AdminiRootController::admin_url(),
774
            $segment,
775
            '/', // trailing slash needed if $action is null!
776
            "$action"
777
        );
778
        $this->extend('updateLink', $link);
779
        return $link;
780
    }
781
782
    /**
783
     * Get menu title for this section (translated)
784
     *
785
     * @param string $class Optional class name if called on LeftAndMain directly
786
     * @param bool $localise Determine if menu title should be localised via i18n.
787
     * @return string Menu title for the given class
788
     */
789
    public static function menu_title($class = null, $localise = true)
790
    {
791
        if ($class && is_subclass_of($class, __CLASS__)) {
792
            // Respect oveloading of menu_title() in subclasses
793
            return $class::menu_title(null, $localise);
794
        }
795
        if (!$class) {
796
            $class = get_called_class();
797
        }
798
799
        // Get default class title
800
        $title = static::config()->get('menu_title');
801
        if (!$title) {
802
            $title = preg_replace('/Admin$/', '', $class);
803
        }
804
805
        // Check localisation
806
        if (!$localise) {
807
            return $title;
808
        }
809
        return i18n::_t("{$class}.MENUTITLE", $title);
810
    }
811
812
    /**
813
     * Return the name for the menu icon
814
     * @param string $class
815
     * @return string
816
     */
817
    public static function menu_icon_for_class($class)
818
    {
819
        return Config::inst()->get($class, 'menu_icon');
820
    }
821
822
    /**
823
     * @param HTTPRequest $request
824
     * @return HTTPResponse|DBHTMLText
825
     * @throws HTTPResponse_Exception
826
     */
827
    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

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

1042
        $form->saveInto($record, /** @scrutinizer ignore-type */ true);
Loading history...
1043
        $record->write();
1044
        $this->extend('onAfterSave', $record);
1045
1046
        //TODO: investigate if this is needed
1047
        // $this->setCurrentPageID($record->ID);
1048
1049
        $message = _t(__CLASS__ . '.SAVEDUP', 'Saved.');
1050
        $this->sessionMessage($message, "good");
1051
        return $this->redirectBack();
1052
    }
1053
1054
    /**
1055
     * Create new item.
1056
     *
1057
     * @param string|int $id
1058
     * @param bool $setID
1059
     * @return DataObject
1060
     */
1061
    public function getNewItem($id, $setID = true)
1062
    {
1063
        $class = $this->config()->get('tree_class');
1064
        $object = Injector::inst()->create($class);
1065
        if ($setID) {
1066
            $object->ID = $id;
1067
        }
1068
        return $object;
1069
    }
1070
1071
    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

1071
    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...
1072
    {
1073
        $className = $this->config()->get('tree_class');
1074
1075
        $id = $data['ID'];
1076
        $record = DataObject::get_by_id($className, $id);
1077
        if ($record && !$record->canDelete()) {
1078
            return Security::permissionFailure();
1079
        }
1080
        if (!$record || !$record->ID) {
1081
            $this->httpError(404, "Bad record ID #" . (int)$id);
1082
        }
1083
1084
        $record->delete();
1085
        $this->sessionMessage(_t(__CLASS__ . '.DELETED', 'Deleted.'));
1086
        return $this->redirectBack();
1087
    }
1088
1089
    /**
1090
     * Retrieves an edit form, either for display, or to process submitted data.
1091
     * Also used in the template rendered through {@link Right()} in the $EditForm placeholder.
1092
     *
1093
     * This is a "pseudo-abstract" methoed, usually connected to a {@link getEditForm()}
1094
     * method in an entwine subclass. This method can accept a record identifier,
1095
     * selected either in custom logic, or through {@link currentPageID()}.
1096
     * The form usually construct itself from {@link DataObject->getCMSFields()}
1097
     * for the specific managed subclass defined in {@link LeftAndMain::$tree_class}.
1098
     *
1099
     * @param HTTPRequest $request Passed if executing a HTTPRequest directly on the form.
1100
     * If empty, this is invoked as $EditForm in the template
1101
     * @return Form Should return a form regardless wether a record has been found.
1102
     *  Form might be readonly if the current user doesn't have the permission to edit
1103
     *  the record.
1104
     */
1105
    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

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