LeftAndMain   F
last analyzed

Complexity

Total Complexity 215

Size/Duplication

Total Lines 1608
Duplicated Lines 0 %

Importance

Changes 11
Bugs 3 Features 0
Metric Value
eloc 585
c 11
b 3
f 0
dl 0
loc 1608
rs 2
wmc 215

63 Methods

Rating   Name   Duplication   Size   Complexity  
A includeLastIcon() 0 24 1
A getRequiredPermissions() 0 15 4
A UseBootstrap5() 0 3 1
B canView() 0 36 10
A includeGoogleFont() 0 12 2
B extendedCanView() 0 42 9
A LinkHash() 0 12 3
A replaceServices() 0 12 4
A ApplicationLink() 0 3 1
A SiteConfig() 0 3 2
A httpError() 0 12 1
A menu_title() 0 21 6
A show() 0 8 1
A MenuCurrentItem() 0 4 1
A BaseCSSClasses() 0 3 1
A getTemplatesWithSuffix() 0 4 1
A Locale() 0 3 1
A Menu() 0 3 1
A MainIcon() 0 13 3
A getVersionProvider() 0 6 2
A SectionTitle() 0 13 4
D MainMenu() 0 88 19
A EmptyForm() 0 16 1
A LogoutURL() 0 4 1
A EditFormTools() 0 8 2
A redirect() 0 4 1
A getNewItem() 0 8 2
A setVersionProvider() 0 4 1
A getSchemaRequested() 0 3 1
A handleRequest() 0 24 3
A redirectWithStatus() 0 5 1
A AdminDir() 0 6 1
A Breadcrumbs() 0 10 2
D loadExtraRequirements() 0 48 18
A getRecord() 0 20 6
A SessionKeepAlivePing() 0 3 1
A HasMinimenu() 0 3 1
B init() 0 77 7
A CMSVersion() 0 3 1
A PreloadFonts() 0 17 4
A sessionNamespace() 0 4 2
A getHelpLinks() 0 21 3
A currentPage() 0 3 1
B providePermissions() 0 46 6
A getApplicationName() 0 3 1
A Content() 0 3 1
A menu_icon_for_class() 0 3 1
A Title() 0 4 2
A Tools() 0 8 2
A Link() 0 23 3
A EditForm() 0 3 1
A setCMSTabset() 0 4 2
A redirectForAjax() 0 12 3
A isCurrentPage() 0 3 1
A includeThemeVariables() 0 20 3
B save() 0 33 8
A printable() 0 17 2
B currentPageID() 0 26 10
A includeFavicon() 0 20 2
A index() 0 3 1
A delete() 0 16 5
F getEditForm() 0 89 20
A CMSVersionNumber() 0 12 3

How to fix   Complexity   

Complex Class

Complex classes like LeftAndMain often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use LeftAndMain, and based on these observations, apply Extract Interface, too.

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

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

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

1128
        $form->saveInto($record, /** @scrutinizer ignore-type */ true);
Loading history...
1129
        $record->write();
1130
        $this->extend('onAfterSave', $record);
1131
1132
        //TODO: investigate if this is needed
1133
        // $this->setCurrentPageID($record->ID);
1134
1135
        $message = _t(__CLASS__ . '.SAVEDUP', 'Saved.');
1136
        $this->sessionMessage($message, "good");
1137
        return $this->redirectBack();
1138
    }
1139
1140
    /**
1141
     * Create new item.
1142
     *
1143
     * @param string|int $id
1144
     * @param bool $setID
1145
     * @return DataObject
1146
     */
1147
    public function getNewItem($id, $setID = true)
1148
    {
1149
        $class = $this->config()->get('tree_class');
1150
        $object = Injector::inst()->create($class);
1151
        if ($setID) {
1152
            $object->ID = $id;
1153
        }
1154
        return $object;
1155
    }
1156
1157
    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

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

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