Passed
Push — master ( 06fadb...21c0e7 )
by Thomas
04:14 queued 01:30
created

LeftAndMain::redirectForAjax()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 12
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

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

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

filter:
    dependency_paths: ["lib/*"]

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

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

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

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

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

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

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