Passed
Push — master ( 2cee16...0c9103 )
by Thomas
02:55
created

LeftAndMain::MainIcon()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 13
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

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

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

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

1094
        $form->saveInto($record, /** @scrutinizer ignore-type */ true);
Loading history...
1095
        $record->write();
1096
        $this->extend('onAfterSave', $record);
1097
1098
        //TODO: investigate if this is needed
1099
        // $this->setCurrentPageID($record->ID);
1100
1101
        $message = _t(__CLASS__ . '.SAVEDUP', 'Saved.');
1102
        $this->sessionMessage($message, "good");
1103
        return $this->redirectBack();
1104
    }
1105
1106
    /**
1107
     * Create new item.
1108
     *
1109
     * @param string|int $id
1110
     * @param bool $setID
1111
     * @return DataObject
1112
     */
1113
    public function getNewItem($id, $setID = true)
1114
    {
1115
        $class = $this->config()->get('tree_class');
1116
        $object = Injector::inst()->create($class);
1117
        if ($setID) {
1118
            $object->ID = $id;
1119
        }
1120
        return $object;
1121
    }
1122
1123
    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

1123
    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...
1124
    {
1125
        $className = $this->config()->get('tree_class');
1126
1127
        $id = $data['ID'];
1128
        $record = DataObject::get_by_id($className, $id);
1129
        if ($record && !$record->canDelete()) {
1130
            return Security::permissionFailure();
1131
        }
1132
        if (!$record || !$record->ID) {
1133
            $this->httpError(404, "Bad record ID #" . (int)$id);
1134
        }
1135
1136
        $record->delete();
1137
        $this->sessionMessage(_t(__CLASS__ . '.DELETED', 'Deleted.'));
1138
        return $this->redirectBack();
1139
    }
1140
1141
    /**
1142
     * Retrieves an edit form, either for display, or to process submitted data.
1143
     * Also used in the template rendered through {@link Right()} in the $EditForm placeholder.
1144
     *
1145
     * This is a "pseudo-abstract" methoed, usually connected to a {@link getEditForm()}
1146
     * method in an entwine subclass. This method can accept a record identifier,
1147
     * selected either in custom logic, or through {@link currentPageID()}.
1148
     * The form usually construct itself from {@link DataObject->getCMSFields()}
1149
     * for the specific managed subclass defined in {@link LeftAndMain::$tree_class}.
1150
     *
1151
     * @param HTTPRequest $request Passed if executing a HTTPRequest directly on the form.
1152
     * If empty, this is invoked as $EditForm in the template
1153
     * @return Form Should return a form regardless wether a record has been found.
1154
     *  Form might be readonly if the current user doesn't have the permission to edit
1155
     *  the record.
1156
     */
1157
    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

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