Passed
Push — master ( b11930...40d9d3 )
by Thomas
12:46 queued 01:32
created

LeftAndMain::replaceServices()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 12
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

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

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

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

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

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

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