Passed
Push — master ( 40d9d3...8b895a )
by Thomas
03:05
created

LeftAndMain::loadExtraRequirements()   D

Complexity

Conditions 18
Paths 24

Size

Total Lines 48
Code Lines 33

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 18
eloc 33
c 1
b 0
f 0
nc 24
nop 0
dl 0
loc 48
rs 4.8666

How to fix   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\Forms\FormAction;
22
use SilverStripe\Forms\HiddenField;
23
use SilverStripe\Security\Security;
24
use SilverStripe\View\Requirements;
25
use SilverStripe\Control\Controller;
26
use SilverStripe\Core\Config\Config;
27
use LeKoala\DeferBackend\CspProvider;
28
use SilverStripe\Control\HTTPRequest;
29
use SilverStripe\Security\Permission;
30
use SilverStripe\Versioned\Versioned;
31
use LeKoala\DeferBackend\DeferBackend;
32
use SilverStripe\Control\HTTPResponse;
33
use LeKoala\Admini\Traits\JsonResponse;
34
use SilverStripe\ORM\FieldType\DBField;
35
use SilverStripe\SiteConfig\SiteConfig;
36
use LeKoala\Admini\Subsites\HasSubsites;
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;
0 ignored issues
show
introduced by
The trait LeKoala\Admini\Traits\Toasts requires some properties which are not provided by LeKoala\Admini\LeftAndMain: $Message, $Type, $ThemeColor
Loading history...
62
    use HasSubsites;
0 ignored issues
show
introduced by
The trait LeKoala\Admini\Subsites\HasSubsites requires some properties which are not provided by LeKoala\Admini\LeftAndMain: $DefaultSite, $Title
Loading history...
63
64
    /**
65
     * The current url segment attached to the LeftAndMain instance
66
     *
67
     * @config
68
     * @var string
69
     */
70
    private static $url_segment = null;
71
72
    /**
73
     * @config
74
     * @var string Used by {@link AdminiRootController} to augment Director route rules for sub-classes of LeftAndMain
75
     */
76
    private static $url_rule = '/$Action/$ID/$OtherID';
77
78
    /**
79
     * @config
80
     * @var string
81
     */
82
    private static $menu_title;
83
84
    /**
85
     * An icon name for last-icon. You can check MaterialIcons class for values
86
     * @config
87
     * @var string
88
     */
89
    private static $menu_icon;
0 ignored issues
show
introduced by
The private property $menu_icon is not used, and could be removed.
Loading history...
90
91
    /**
92
     * @config
93
     * @var int
94
     */
95
    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...
96
97
    /**
98
     * @config
99
     * @var int
100
     */
101
    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...
102
103
    /**
104
     * A subclass of {@link DataObject}.
105
     *
106
     * Determines what is managed in this interface, through
107
     * {@link getEditForm()} and other logic.
108
     *
109
     * @config
110
     * @var string
111
     */
112
    private static $tree_class = null;
113
114
    /**
115
     * @var array
116
     */
117
    private static $allowed_actions = [
118
        'index',
119
        'save',
120
        'printable',
121
        'show',
122
        'EditForm',
123
    ];
124
125
    /**
126
     * Assign themes to use for cms
127
     *
128
     * @config
129
     * @var array
130
     */
131
    private static $admin_themes = [
0 ignored issues
show
introduced by
The private property $admin_themes is not used, and could be removed.
Loading history...
132
        'lekoala/silverstripe-admini:forms',
133
        SSViewer::DEFAULT_THEME,
134
    ];
135
136
    /**
137
     * Codes which are required from the current user to view this controller.
138
     * If multiple codes are provided, all of them are required.
139
     * All CMS controllers require "CMS_ACCESS_LeftAndMain" as a baseline check,
140
     * and fall back to "CMS_ACCESS_<class>" if no permissions are defined here.
141
     * See {@link canView()} for more details on permission checks.
142
     *
143
     * @config
144
     * @var array
145
     */
146
    private static $required_permission_codes;
147
148
    /**
149
     * Namespace for session info, e.g. current record.
150
     * Defaults to the current class name, but can be amended to share a namespace in case
151
     * controllers are logically bundled together, and mainly separated
152
     * to achieve more flexible templating.
153
     *
154
     * @config
155
     * @var string
156
     */
157
    private static $session_namespace;
0 ignored issues
show
introduced by
The private property $session_namespace is not used, and could be removed.
Loading history...
158
159
    /**
160
     * Register additional requirements through the {@link Requirements} class.
161
     * Used mainly to work around the missing "lazy loading" functionality
162
     * for getting css/javascript required after an ajax-call (e.g. loading the editform).
163
     *
164
     * YAML configuration example:
165
     * <code>
166
     * LeftAndMain:
167
     *   extra_requirements_javascript:
168
     *     - mysite/javascript/myscript.js
169
     * </code>
170
     *
171
     * @config
172
     * @var array
173
     */
174
    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...
175
176
    /**
177
     * YAML configuration example:
178
     * <code>
179
     * LeftAndMain:
180
     *   extra_requirements_css:
181
     *     mysite/css/mystyle.css:
182
     *       media: screen
183
     * </code>
184
     *
185
     * @config
186
     * @var array See {@link extra_requirements_javascript}
187
     */
188
    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...
189
190
    /**
191
     * @config
192
     * @var array See {@link extra_requirements_javascript}
193
     */
194
    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...
195
196
    /**
197
     * If true, call a keepalive ping every 5 minutes from the CMS interface,
198
     * to ensure that the session never dies.
199
     *
200
     * @config
201
     * @var bool
202
     */
203
    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...
204
205
    /**
206
     * Value of X-Frame-Options header
207
     *
208
     * @config
209
     * @var string
210
     */
211
    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...
212
213
    /**
214
     * The configuration passed to the supporting JS for each CMS section includes a 'name' key
215
     * that by default matches the FQCN of the current class. This setting allows you to change
216
     * the key if necessary (for example, if you are overloading CMSMain or another core class
217
     * and want to keep the core JS - which depends on the core class names - functioning, you
218
     * would need to set this to the FQCN of the class you are overloading).
219
     *
220
     * @config
221
     * @var string|null
222
     */
223
    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...
224
225
    /**
226
     * @var array
227
     * @config
228
     */
229
    private static $casting = [
0 ignored issues
show
introduced by
The private property $casting is not used, and could be removed.
Loading history...
230
        'MainIcon' => 'HTMLText'
231
    ];
232
233
    /**
234
     * The urls used for the links in the Help dropdown in the backend
235
     *
236
     * @config
237
     * @var array
238
     */
239
    private static $help_links = [
0 ignored issues
show
introduced by
The private property $help_links is not used, and could be removed.
Loading history...
240
        'CMS User help' => 'https://userhelp.silverstripe.org/en/4',
241
        'Developer docs' => 'https://docs.silverstripe.org/en/4/',
242
        'Community' => 'https://www.silverstripe.org/',
243
        'Feedback' => 'https://www.silverstripe.org/give-feedback/',
244
    ];
245
246
    /**
247
     * The href for the anchor on the Silverstripe logo
248
     *
249
     * @config
250
     * @var string
251
     */
252
    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...
253
254
    /**
255
     * The application name
256
     *
257
     * @config
258
     * @var string
259
     */
260
    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...
261
262
    /**
263
     * Current pageID for this request
264
     *
265
     * @var null
266
     */
267
    protected $pageID = null;
268
269
    /**
270
     * @var VersionProvider
271
     */
272
    protected $versionProvider;
273
274
    /**
275
     * @param Member $member
276
     * @return bool
277
     */
278
    public function canView($member = null)
279
    {
280
        if (!$member && $member !== false) {
0 ignored issues
show
introduced by
The condition $member !== false is always false.
Loading history...
281
            $member = Security::getCurrentUser();
282
        }
283
284
        // cms menus only for logged-in members
285
        if (!$member) {
286
            return false;
287
        }
288
289
        // alternative extended checks
290
        if ($this->hasMethod('alternateAccessCheck')) {
291
            $alternateAllowed = $this->alternateAccessCheck($member);
292
            if ($alternateAllowed === false) {
293
                return false;
294
            }
295
        }
296
297
        // Check for "CMS admin" permission
298
        if (Permission::checkMember($member, "CMS_ACCESS_LeftAndMain")) {
299
            return true;
300
        }
301
302
        // Check for LeftAndMain sub-class permissions
303
        $codes = $this->getRequiredPermissions();
304
        if ($codes === false) { // allow explicit FALSE to disable subclass check
305
            return true;
306
        }
307
        foreach ((array)$codes as $code) {
308
            if (!Permission::checkMember($member, $code)) {
309
                return false;
310
            }
311
        }
312
313
        return true;
314
    }
315
316
    /**
317
     * Get list of required permissions
318
     *
319
     * @return array|string|bool Code, array of codes, or false if no permission required
320
     */
321
    public static function getRequiredPermissions()
322
    {
323
        $class = get_called_class();
324
        // If the user is accessing LeftAndMain directly, only generic permissions are required.
325
        if ($class === self::class) {
326
            return 'CMS_ACCESS';
327
        }
328
        $code = Config::inst()->get($class, 'required_permission_codes');
329
        if ($code === false) {
330
            return false;
331
        }
332
        if ($code) {
333
            return $code;
334
        }
335
        return 'CMS_ACCESS_' . $class;
336
    }
337
338
    protected function includeGoogleFont()
339
    {
340
        $font = self::config()->google_font;
341
        if (!$font) {
342
            return;
343
        }
344
        $preconnect = <<<HTML
345
<link rel="preconnect" href="https://fonts.googleapis.com" />
346
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
347
HTML;
348
        Requirements::insertHeadTags($preconnect, "googlefontspreconnect");
349
        Requirements::css("https://fonts.googleapis.com/css2?$font");
350
    }
351
352
    protected function includeLastIcon()
353
    {
354
        $preconnect = <<<HTML
355
<link rel="preconnect" href="https://fonts.googleapis.com" />
356
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
357
HTML;
358
        Requirements::insertHeadTags($preconnect, "googlefontspreconnect");
359
360
        // Could also host locally https://marella.me/material-icons/demo/#two-tone
361
        Requirements::css("https://fonts.googleapis.com/icon?family=Material+Icons+Two+Tone");
362
        Requirements::javascript('lekoala/silverstripe-admini: client/js/last-icon.min.js', ["type" => "application/javascript"]);
363
        $nonce = CspProvider::getCspNonce();
364
        $lastIconScript = <<<JS
365
<script nonce="$nonce">
366
    window.LastIcon = {
367
            types: {
368
            material: "twotone",
369
            },
370
            defaultSet: "material",
371
            fonts: ["material"],
372
        };
373
</script>
374
JS;
375
        Requirements::insertHeadTags($lastIconScript, __FUNCTION__);
376
    }
377
378
    public function UseBootstrap5()
379
    {
380
        return true;
381
    }
382
383
    protected function includeFavicon()
384
    {
385
        $icon = $this->MainIcon();
386
        if (strpos($icon, "<img") === 0) {
387
            // Regular image
388
            $matches = [];
389
            preg_match('/src="([^"]+)"/', $icon, $matches);
390
            $url = $matches[1] ?? '';
391
            $html = <<<HTML
392
<link rel="icon" type="image/png" href="$url" />
393
HTML;
394
        } else {
395
            // Svg icon
396
            $encodedIcon = str_replace(['"', '#'], ['%22', '%23'], $icon);
397
            $html = <<<HTML
398
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,$encodedIcon" />
399
HTML;
400
        }
401
402
        Requirements::insertHeadTags($html, __FUNCTION__);
403
    }
404
405
    protected function includeThemeVariables()
406
    {
407
        $SiteConfig = SiteConfig::current_site_config();
408
        if (!$SiteConfig->PrimaryColor) {
409
            return;
410
        }
411
        $PrimaryColor = $SiteConfig->dbObject('PrimaryColor');
412
        if (!$PrimaryColor->hasMethod('Color')) {
413
            return;
414
        }
415
416
        $bg = $PrimaryColor->Color();
417
        // Black is too harsh
418
        $color = $PrimaryColor->ContrastColor('#020C11');
419
        $border = $PrimaryColor->HighlightColor();
0 ignored issues
show
Unused Code introduced by
The assignment to $border is dead and can be removed.
Loading history...
420
421
        $styles = <<<CSS
422
.sidebar-brand {background: $bg; color: $color}
423
CSS;
424
        Requirements::customCSS($styles, __FUNCTION__);
425
    }
426
427
    /**
428
     * @return int
429
     */
430
    public function SessionKeepAlivePing()
431
    {
432
        return LeftAndMain::config()->uninherited('session_keepalive_ping');
433
    }
434
435
    /**
436
     * The icon to be used either as favicon or in the menu
437
     * Can return a <svg> or <img> tag
438
     */
439
    public function MainIcon(): string
440
    {
441
        // Can be provided by the SiteConfig as a svg string or an uploaded png
442
        $SiteConfig = SiteConfig::current_site_config();
443
        if ($SiteConfig->hasMethod('SvgIcon')) {
444
            return $SiteConfig->SvgIcon();
445
        }
446
        if ($SiteConfig->IconID) {
447
            return '<img src="' . ($SiteConfig->Icon()->Link() ?? '') . '" />';
448
        }
449
        $emoji = self::config()->svg_emoji ?? '💠';
450
        $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>';
451
        return $icon;
452
    }
453
454
    public function AdminDir(): string
455
    {
456
        $path = "js/admini.js";
457
        $resource = ModuleLoader::getModule('lekoala/silverstripe-base')->getResource($path);
458
        $dir = dirname($resource->getRelativePath());
459
        return $dir;
460
    }
461
462
    /**
463
     * Preload fonts
464
     */
465
    public function PreloadFonts()
466
    {
467
        $fonts = self::config()->preload_fonts;
468
        if (empty($fonts)) {
469
            return;
470
        }
471
        $dir = $this->AdminDir();
472
473
        $html = '';
474
        if (!empty($fonts)) {
475
            foreach ($fonts as $font) {
476
                $font = $dir . $font;
477
                // browsers will ignore preloaded fonts without the crossorigin attribute, which will cause the browser to actually fetch the font twice
478
                $html .= "<link rel=\"preload\" href=\"$font\" as=\"font\" type=\"font/woff2\" crossOrigin=\"anonymous\" >\n";
479
            }
480
        }
481
        Requirements::insertHeadTags($html, __FUNCTION__);
482
    }
483
484
    public function HasMinimenu(): bool
485
    {
486
        return (bool)Cookie::get("minimenu");
487
    }
488
489
    /**
490
     * In the CMS, we use tabs in the header
491
     * Do not render actual tabs from the form
492
     *
493
     * @param Form $form
494
     * @return void
495
     */
496
    public function setCMSTabset(Form $form)
497
    {
498
        if ($form->Fields()->hasTabSet()) {
499
            $form->Fields()->findOrMakeTab('Root')->setTemplate('SilverStripe\\Forms\\CMSTabSet');
500
        }
501
    }
502
503
    /**
504
     * Check if the current request has a X-Formschema-Request header set.
505
     * Used by conditional logic that responds to validation results
506
     *
507
     * @return bool
508
     */
509
    protected function getSchemaRequested()
510
    {
511
        return false;
512
    }
513
514
    protected function loadExtraRequirements()
515
    {
516
        $extraJs = $this->config()->get('extra_requirements_javascript');
517
        if ($extraJs) {
518
            foreach ($extraJs as $file => $config) {
519
                if (is_numeric($file)) {
520
                    if (is_string($config)) {
521
                        $file = $config;
522
                        $config = [];
523
                    } elseif (is_array($config)) {
524
                        $file = $config['src'];
525
                        unset($config['src']);
526
                    }
527
                }
528
                Requirements::javascript($file, $config);
529
            }
530
        }
531
532
        $extraCss = $this->config()->get('extra_requirements_css');
533
        if ($extraCss) {
534
            foreach ($extraCss as $file => $config) {
535
                if (is_numeric($file)) {
536
                    if (is_string($config)) {
537
                        $file = $config;
538
                        $config = [];
539
                    } elseif (is_array($config)) {
540
                        $file = $config['href'];
541
                        unset($config['href']);
542
                    }
543
                }
544
545
                Requirements::css($file, isset($config['media']) ? $config['media'] : null);
546
            }
547
        }
548
549
        $extraThemedCss = $this->config()->get('extra_requirements_themedCss');
550
        if ($extraThemedCss) {
551
            foreach ($extraThemedCss as $file => $config) {
552
                if (is_numeric($file)) {
553
                    if (is_string($config)) {
554
                        $file = $config;
555
                        $config = [];
556
                    } elseif (is_array($config)) {
557
                        $file = $config['href'];
558
                        unset($config['href']);
559
                    }
560
                }
561
                Requirements::themedCSS($file, isset($config['media']) ? $config['media'] : null);
562
            }
563
        }
564
    }
565
566
    /**
567
     * @uses LeftAndMainExtension->init()
568
     * @uses LeftAndMainExtension->accessedCMS()
569
     * @uses CMSMenu
570
     */
571
    protected function init()
572
    {
573
        parent::init();
574
575
        // SSViewer::config()->source_file_comments = true;
576
577
        // Lazy by default
578
        Config::modify()->set(\LeKoala\Tabulator\TabulatorGrid::class, "default_lazy_init", true);
579
580
        // Pure modal
581
        Config::modify()->set(\LeKoala\PureModal\PureModal::class, "move_modal_to_body", true);
0 ignored issues
show
Bug introduced by
The type LeKoala\PureModal\PureModal was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
582
583
        // Replace classes
584
        self::replaceServices();
585
        DeferBackend::config()->enable_js_modules = true;
586
        DeferBackend::replaceBackend();
587
588
        $this->showToasterMessage();
589
        $this->blockSubsiteRequirements();
590
591
        HTTPCacheControlMiddleware::singleton()->disableCache();
592
593
        SSViewer::setRewriteHashLinksDefault(false);
594
        ContentNegotiator::setEnabled(false);
595
596
        // set language based on current user locale
597
        $member = Security::getCurrentUser();
598
        if (!empty($member->Locale)) {
599
            i18n::set_locale($member->Locale);
600
        }
601
602
        $response = $this->extendedCanView();
603
        if ($response) {
604
            return $response;
605
        }
606
607
        // Don't continue if there's already been a redirection request.
608
        if ($this->redirectedTo()) {
609
            //TODO: check why we return nothing?
610
            return;
611
        }
612
613
        // Audit logging hook
614
        if (empty($_REQUEST['executeForm']) && !$this->getRequest()->isAjax()) {
615
            $this->extend('accessedCMS');
616
        }
617
618
        $this->includeFavicon();
619
        $this->includeLastIcon();
620
        $this->includeGoogleFont();
621
        $this->includeThemeVariables();
622
623
        Requirements::javascript('lekoala/silverstripe-admini: client/js/admini.min.js');
624
        // This must be applied last, so we put it at the bottom manually because requirements backend may inject stuff in the body
625
        // Requirements::customScript("window.admini.init()");
626
        Requirements::css('lekoala/silverstripe-admini: client/css/admini.min.css');
627
        Requirements::css('lekoala/silverstripe-admini: client/css/custom.css');
628
629
        //TODO: restore these features
630
        // Requirements::add_i18n_javascript('silverstripe/admin:client/lang');
631
        // Requirements::add_i18n_javascript('silverstripe/admin:client/dist/moment-locales', false, false, true);
632
633
        // if (LeftAndMain::config()->uninherited('session_keepalive_ping')) {
634
        //     Requirements::javascript('silverstripe/admin: client/dist/js/LeftAndMain.Ping.js');
635
        // }
636
637
        $this->loadExtraRequirements();
638
        $this->extend('init');
639
640
        // Assign default cms theme and replace user-specified themes
641
        // This allows us for instance to set custom form templates for BS5
642
        SSViewer::set_themes(LeftAndMain::config()->uninherited('admin_themes'));
643
644
        // Versioned support
645
        if (class_exists(Versioned::class)) {
646
            // Make ide happy by not using a potentially undefined class
647
            $class = Versioned::class;
648
            // Set the current reading mode
649
            $class::set_stage($class::DRAFT);
650
            // Set default reading mode to suppress ?stage=Stage querystring params in CMS
651
            $class::set_default_reading_mode($class::get_reading_mode());
652
        }
653
    }
654
655
    protected static function replaceServices()
656
    {
657
        $replacementServices = self::config()->replacement_services;
658
        if ($replacementServices) {
659
            $replacementConfig = [];
660
            foreach ($replacementServices as $replaced => $replacement) {
661
                if (!class_exists($replacement['class'])) {
662
                    continue;
663
                }
664
                $replacementConfig[$replaced] = $replacement;
665
            }
666
            Injector::inst()->load($replacementConfig);
667
        }
668
    }
669
670
    /**
671
     * Returns the link with the posted hash if any
672
     * Depends on a _hash input in the
673
     *
674
     * @param string $action
675
     * @return string
676
     */
677
    public function LinkHash($action = null): string
678
    {
679
        $request = $this->getRequest();
680
        $hash = null;
681
        if ($request->isPOST()) {
682
            $hash = $request->postVar("_hash");
683
        }
684
        $link = $this->Link($action);
685
        if ($hash) {
686
            $link .= $hash;
687
        }
688
        return $link;
689
    }
690
691
    /**
692
     * Allow customisation of the access check by a extension
693
     * Also all the canView() check to execute Controller::redirect()
694
     * @return HTTPResponse|null
695
     */
696
    protected function extendedCanView()
697
    {
698
        if ($this->canView() || $this->getResponse()->isFinished()) {
699
            return null;
700
        }
701
702
        // When access /admin/, we should try a redirect to another part of the admin rather than be locked out
703
        $menu = $this->MainMenu();
704
        foreach ($menu as $candidate) {
705
            $canView = $candidate->Link &&
706
                $candidate->Link != $this->Link()
707
                && $candidate->MenuItem->controller
708
                && singleton($candidate->MenuItem->controller)->canView();
709
            if ($canView) {
710
                $this->redirect($candidate->Link);
711
                return;
712
            }
713
        }
714
715
        if (Security::getCurrentUser()) {
716
            $this->getRequest()->getSession()->clear("BackURL");
717
        }
718
719
        // if no alternate menu items have matched, return a permission error
720
        $messageSet = array(
721
            'default' => _t(
722
                __CLASS__ . '.PERMDEFAULT',
723
                "You must be logged in to access the administration area; please enter your credentials below."
724
            ),
725
            'alreadyLoggedIn' => _t(
726
                __CLASS__ . '.PERMALREADY',
727
                "I'm sorry, but you can't access that part of the CMS.  If you want to log in as someone else, do"
728
                    . " so below."
729
            ),
730
            'logInAgain' => _t(
731
                __CLASS__ . '.PERMAGAIN',
732
                "You have been logged out of the CMS.  If you would like to log in again, enter a username and"
733
                    . " password below."
734
            ),
735
        );
736
737
        return Security::permissionFailure($this, $messageSet);
738
    }
739
740
    public function handleRequest(HTTPRequest $request)
741
    {
742
        try {
743
            $response = parent::handleRequest($request);
744
        } catch (ValidationException $e) {
745
            // Nicer presentation of model-level validation errors
746
            $msgs = _t(__CLASS__ . '.ValidationError', 'Validation error') . ': '
747
                . $e->getMessage();
748
            $this->sessionMessage($msgs, "bad");
749
            return $this->redirectBack();
750
        }
751
752
        $title = $this->Title();
753
754
        //TODO: check this when implementing ajax
755
        if (!$response->getHeader('X-Controller')) {
756
            $response->addHeader('X-Controller', static::class);
757
        }
758
        if (!$response->getHeader('X-Title')) {
759
            $response->addHeader('X-Title', urlencode($title));
760
        }
761
762
        // Prevent clickjacking, see https://developer.mozilla.org/en-US/docs/HTTP/X-Frame-Options
763
        $originalResponse = $this->getResponse();
764
        $originalResponse->addHeader('X-Frame-Options', LeftAndMain::config()->uninherited('frame_options'));
765
        $originalResponse->addHeader('Vary', 'X-Requested-With');
766
767
        return $response;
768
    }
769
770
    /**
771
     * Overloaded redirection logic to trigger a fake redirect on ajax requests.
772
     * While this violates HTTP principles, its the only way to work around the
773
     * fact that browsers handle HTTP redirects opaquely, no intervention via JS is possible.
774
     * In isolation, that's not a problem - but combined with history.pushState()
775
     * it means we would request the same redirection URL twice if we want to update the URL as well.
776
     * See LeftAndMain.js for the required jQuery ajaxComplete handlers.
777
     *
778
     * @param string $url
779
     * @param int $code
780
     * @return HTTPResponse|string
781
     */
782
    public function redirect($url, $code = 302)
783
    {
784
        //TODO: check this when implementing ajax navigation
785
        if ($this->getRequest()->isAjax()) {
786
            $response = $this->getResponse();
787
            $response->addHeader('X-ControllerURL', $url);
788
            if ($this->getRequest()->getHeader('X-Pjax') && !$response->getHeader('X-Pjax')) {
789
                $response->addHeader('X-Pjax', $this->getRequest()->getHeader('X-Pjax'));
790
            }
791
            $newResponse = new HTTPResponse(
792
                $response->getBody(),
793
                $response->getStatusCode(),
794
                $response->getStatusDescription()
795
            );
796
            foreach ($response->getHeaders() as $k => $v) {
797
                $newResponse->addHeader($k, $v);
798
            }
799
800
            // $newResponse->setIsFinished(true);
801
            // $this->setResponse($newResponse);
802
803
            return ''; // Actual response will be re-requested by client
804
        } else {
805
            return parent::redirect($url, $code);
806
        }
807
    }
808
809
    /**
810
     * @param HTTPRequest $request
811
     * @return HTTPResponse|DBHTMLText
812
     */
813
    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

813
    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...
814
    {
815
        return $this->renderWith($this->getViewer('show'));
816
    }
817
818
    /**
819
     * You should implement a Link() function in your subclass of LeftAndMain,
820
     * to point to the URL of that particular controller.
821
     *
822
     * @param string $action
823
     * @return string
824
     */
825
    public function Link($action = null)
826
    {
827
        // LeftAndMain methods have a top-level uri access
828
        if (static::class === LeftAndMain::class) {
0 ignored issues
show
introduced by
The condition static::class === LeKoal...mini\LeftAndMain::class is always true.
Loading history...
829
            $segment = '';
830
        } else {
831
            // Get url_segment
832
            $segment = $this->config()->get('url_segment');
833
            if (!$segment) {
834
                throw new BadMethodCallException(
835
                    sprintf('LeftAndMain subclasses (%s) must have url_segment', static::class)
836
                );
837
            }
838
        }
839
840
        $link = Controller::join_links(
841
            AdminiRootController::admin_url(),
842
            $segment,
843
            '/', // trailing slash needed if $action is null!
844
            "$action"
845
        );
846
        $this->extend('updateLink', $link);
847
        return $link;
848
    }
849
850
    /**
851
     * Get menu title for this section (translated)
852
     *
853
     * @param string $class Optional class name if called on LeftAndMain directly
854
     * @param bool $localise Determine if menu title should be localised via i18n.
855
     * @return string Menu title for the given class
856
     */
857
    public static function menu_title($class = null, $localise = true)
858
    {
859
        if ($class && is_subclass_of($class, __CLASS__)) {
860
            // Respect oveloading of menu_title() in subclasses
861
            return $class::menu_title(null, $localise);
862
        }
863
        if (!$class) {
864
            $class = get_called_class();
865
        }
866
867
        // Get default class title
868
        $title = static::config()->get('menu_title');
869
        if (!$title) {
870
            $title = preg_replace('/Admin$/', '', $class);
871
        }
872
873
        // Check localisation
874
        if (!$localise) {
875
            return $title;
876
        }
877
        return i18n::_t("{$class}.MENUTITLE", $title);
878
    }
879
880
    /**
881
     * Return the name for the menu icon
882
     * @param string $class
883
     * @return string
884
     */
885
    public static function menu_icon_for_class($class)
886
    {
887
        return Config::inst()->get($class, 'menu_icon');
888
    }
889
890
    /**
891
     * @param HTTPRequest $request
892
     * @return HTTPResponse|DBHTMLText
893
     * @throws HTTPResponse_Exception
894
     */
895
    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

895
    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...
896
    {
897
        // TODO Necessary for TableListField URLs to work properly
898
        // TODO: check why this is needed
899
        // if ($request->param('ID')) {
900
        //     $this->setCurrentPageID($request->param('ID'));
901
        // }
902
        return $this->renderWith($this->getViewer('show'));
903
    }
904
905
    //------------------------------------------------------------------------------------------//
906
    // Main UI components
907
908
    /**
909
     * Returns the main menu of the CMS.  This is also used by init()
910
     * to work out which sections the user has access to.
911
     *
912
     * @param bool $cached
913
     * @return SS_List
914
     */
915
    public function MainMenu($cached = true)
916
    {
917
        static $menuCache = null;
918
        if ($menuCache === null || !$cached) {
919
            // Don't accidentally return a menu if you're not logged in - it's used to determine access.
920
            if (!Security::getCurrentUser()) {
921
                return new ArrayList();
922
            }
923
924
            // Encode into DO set
925
            $menu = new ArrayList();
926
            $menuItems = CMSMenu::get_viewable_menu_items();
927
928
            // extra styling for custom menu-icons
929
            $menuIconStyling = '';
930
931
            if ($menuItems) {
932
                /** @var CMSMenuItem $menuItem */
933
                foreach ($menuItems as $code => $menuItem) {
934
                    // alternate permission checks (in addition to LeftAndMain->canView())
935
                    $alternateCheck = isset($menuItem->controller)
936
                        && $this->hasMethod('alternateMenuDisplayCheck')
937
                        && !$this->alternateMenuDisplayCheck($menuItem->controller);
938
                    if ($alternateCheck) {
939
                        continue;
940
                    }
941
942
                    // linking mode
943
                    $linkingmode = "link";
944
                    if ($menuItem->controller && get_class($this) == $menuItem->controller) {
945
                        $linkingmode = "current";
946
                    } elseif (strpos($this->Link(), $menuItem->url) !== false) {
947
                        if ($this->Link() == $menuItem->url) {
948
                            $linkingmode = "current";
949
950
                            // default menu is the one with a blank {@link url_segment}
951
                        } elseif (singleton($menuItem->controller)->config()->get('url_segment') == '') {
952
                            if ($this->Link() == AdminiRootController::admin_url()) {
953
                                $linkingmode = "current";
954
                            }
955
                        } else {
956
                            $linkingmode = "current";
957
                        }
958
                    }
959
960
                    // already set in CMSMenu::populate_menu(), but from a static pre-controller
961
                    // context, so doesn't respect the current user locale in _t() calls - as a workaround,
962
                    // we simply call LeftAndMain::menu_title() again
963
                    // if we're dealing with a controller
964
                    if ($menuItem->controller) {
965
                        $title = LeftAndMain::menu_title($menuItem->controller);
966
                    } else {
967
                        $title = $menuItem->title;
968
                    }
969
970
                    // Provide styling for custom $menu-icon. Done here instead of in
971
                    // CMSMenu::populate_menu(), because the icon is part of
972
                    // the CMS right pane for the specified class as well...
973
                    $IconName = '';
974
                    if ($menuItem->controller) {
975
                        $IconName = LeftAndMain::menu_icon_for_class($menuItem->controller);
976
                    } else {
977
                        $IconName = $menuItem->iconName;
978
                    }
979
                    if (!$IconName) {
980
                        $IconName = "arrow_right";
981
                    }
982
                    $menuItem->addExtraClass("sidebar-link");
983
984
                    $menu->push(new ArrayData([
985
                        "MenuItem" => $menuItem,
986
                        "AttributesHTML" => $menuItem->getAttributesHTML(),
987
                        "Title" => $title,
988
                        "Code" => $code,
989
                        "IconName" => $IconName,
990
                        "Link" => $menuItem->url,
991
                        "LinkingMode" => $linkingmode
992
                    ]));
993
                }
994
            }
995
            if ($menuIconStyling) {
996
                Requirements::customCSS($menuIconStyling);
997
            }
998
999
            $menuCache = $menu;
1000
        }
1001
1002
        return $menuCache;
1003
    }
1004
1005
    public function Menu()
1006
    {
1007
        return $this->renderWith($this->getTemplatesWithSuffix('_Menu'));
1008
    }
1009
1010
    /**
1011
     * @todo Wrap in CMSMenu instance accessor
1012
     * @return ArrayData A single menu entry (see {@link MainMenu})
1013
     */
1014
    public function MenuCurrentItem()
1015
    {
1016
        $items = $this->MainMenu();
1017
        return $items->find('LinkingMode', 'current');
1018
    }
1019
1020
    /**
1021
     * Return appropriate template(s) for this class, with the given suffix using
1022
     * {@link SSViewer::get_templates_by_class()}
1023
     *
1024
     * @param string $suffix
1025
     * @return string|array
1026
     */
1027
    public function getTemplatesWithSuffix($suffix)
1028
    {
1029
        $templates = SSViewer::get_templates_by_class(get_class($this), $suffix, __CLASS__);
1030
        return SSViewer::chooseTemplate($templates);
1031
    }
1032
1033
    public function Content()
1034
    {
1035
        return $this->renderWith($this->getTemplatesWithSuffix('_Content'));
1036
    }
1037
1038
    /**
1039
     * Get dataobject from the current ID
1040
     *
1041
     * @param int|DataObject $id ID or object
1042
     * @return DataObject
1043
     */
1044
    public function getRecord($id = null)
1045
    {
1046
        if ($id === null) {
1047
            $id = $this->currentPageID();
1048
        }
1049
        $className = $this->config()->get('tree_class');
1050
        if (!$className) {
1051
            return null;
1052
        }
1053
        if ($id instanceof $className) {
1054
            /** @var DataObject $id */
1055
            return $id;
1056
        }
1057
        if ($id === 'root') {
0 ignored issues
show
introduced by
The condition $id === 'root' is always false.
Loading history...
1058
            return DataObject::singleton($className);
1059
        }
1060
        if (is_numeric($id)) {
1061
            return DataObject::get_by_id($className, $id);
1062
        }
1063
        return null;
1064
    }
1065
1066
    /**
1067
     * Called by CMSBreadcrumbs.ss
1068
     * @param bool $unlinked
1069
     * @return ArrayList
1070
     */
1071
    public function Breadcrumbs($unlinked = false)
1072
    {
1073
        $items = new ArrayList(array(
1074
            new ArrayData(array(
1075
                'Title' => $this->menu_title(),
1076
                'Link' => ($unlinked) ? false : $this->Link()
1077
            ))
1078
        ));
1079
1080
        return $items;
1081
    }
1082
1083
    /**
1084
     * Save  handler
1085
     *
1086
     * @param array $data
1087
     * @param Form $form
1088
     * @return HTTPResponse
1089
     */
1090
    public function save($data, $form)
1091
    {
1092
        $request = $this->getRequest();
0 ignored issues
show
Unused Code introduced by
The assignment to $request is dead and can be removed.
Loading history...
1093
        $className = $this->config()->get('tree_class');
1094
1095
        // Existing or new record?
1096
        $id = $data['ID'];
1097
        if (is_numeric($id) && $id > 0) {
1098
            $record = DataObject::get_by_id($className, $id);
1099
            if ($record && !$record->canEdit()) {
1100
                return Security::permissionFailure($this);
1101
            }
1102
            if (!$record || !$record->ID) {
1103
                $this->httpError(404, "Bad record ID #" . (int)$id);
1104
            }
1105
        } else {
1106
            if (!singleton($this->config()->get('tree_class'))->canCreate()) {
1107
                return Security::permissionFailure($this);
1108
            }
1109
            $record = $this->getNewItem($id, false);
1110
        }
1111
1112
        // save form data into record
1113
        $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

1113
        $form->saveInto($record, /** @scrutinizer ignore-type */ true);
Loading history...
1114
        $record->write();
1115
        $this->extend('onAfterSave', $record);
1116
1117
        //TODO: investigate if this is needed
1118
        // $this->setCurrentPageID($record->ID);
1119
1120
        $message = _t(__CLASS__ . '.SAVEDUP', 'Saved.');
1121
        $this->sessionMessage($message, "good");
1122
        return $this->redirectBack();
1123
    }
1124
1125
    /**
1126
     * Create new item.
1127
     *
1128
     * @param string|int $id
1129
     * @param bool $setID
1130
     * @return DataObject
1131
     */
1132
    public function getNewItem($id, $setID = true)
1133
    {
1134
        $class = $this->config()->get('tree_class');
1135
        $object = Injector::inst()->create($class);
1136
        if ($setID) {
1137
            $object->ID = $id;
1138
        }
1139
        return $object;
1140
    }
1141
1142
    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

1142
    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...
1143
    {
1144
        $className = $this->config()->get('tree_class');
1145
1146
        $id = $data['ID'];
1147
        $record = DataObject::get_by_id($className, $id);
1148
        if ($record && !$record->canDelete()) {
1149
            return Security::permissionFailure();
1150
        }
1151
        if (!$record || !$record->ID) {
1152
            $this->httpError(404, "Bad record ID #" . (int)$id);
1153
        }
1154
1155
        $record->delete();
1156
        $this->sessionMessage(_t(__CLASS__ . '.DELETED', 'Deleted.'));
1157
        return $this->redirectBack();
1158
    }
1159
1160
    /**
1161
     * Retrieves an edit form, either for display, or to process submitted data.
1162
     * Also used in the template rendered through {@link Right()} in the $EditForm placeholder.
1163
     *
1164
     * This is a "pseudo-abstract" methoed, usually connected to a {@link getEditForm()}
1165
     * method in an entwine subclass. This method can accept a record identifier,
1166
     * selected either in custom logic, or through {@link currentPageID()}.
1167
     * The form usually construct itself from {@link DataObject->getCMSFields()}
1168
     * for the specific managed subclass defined in {@link LeftAndMain::$tree_class}.
1169
     *
1170
     * @param HTTPRequest $request Passed if executing a HTTPRequest directly on the form.
1171
     * If empty, this is invoked as $EditForm in the template
1172
     * @return Form Should return a form regardless wether a record has been found.
1173
     *  Form might be readonly if the current user doesn't have the permission to edit
1174
     *  the record.
1175
     */
1176
    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

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