Passed
Push — master ( ca0b10...15ea5b )
by Thomas
04:25 queued 01:12
created

LeftAndMain::AdminDir()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 4
c 0
b 0
f 0
nc 1
nop 0
dl 0
loc 6
rs 10
1
<?php
2
3
namespace LeKoala\Admini;
4
5
use LogicException;
6
use BadMethodCallException;
7
use SilverStripe\i18n\i18n;
8
use SilverStripe\Forms\Form;
9
use SilverStripe\ORM\SS_List;
10
use SilverStripe\Dev\TestOnly;
11
use SilverStripe\ORM\ArrayList;
12
use SilverStripe\View\SSViewer;
13
use SilverStripe\Control\Cookie;
14
use SilverStripe\Core\ClassInfo;
15
use SilverStripe\ORM\DataObject;
16
use SilverStripe\View\ArrayData;
17
use LeKoala\Admini\Traits\Toasts;
18
use SilverStripe\Dev\Deprecation;
19
use SilverStripe\Forms\FieldList;
20
use SilverStripe\Security\Member;
21
use SilverStripe\Control\Director;
22
use SilverStripe\Forms\FormAction;
23
use SilverStripe\Forms\HiddenField;
24
use SilverStripe\Security\Security;
25
use SilverStripe\View\Requirements;
26
use LeKoala\Tabulator\TabulatorGrid;
27
use SilverStripe\Control\Controller;
28
use SilverStripe\Core\Config\Config;
29
use LeKoala\DeferBackend\CspProvider;
30
use SilverStripe\Control\HTTPRequest;
31
use SilverStripe\Security\Permission;
32
use SilverStripe\Versioned\Versioned;
33
use LeKoala\DeferBackend\DeferBackend;
34
use SilverStripe\Control\HTTPResponse;
35
use SilverStripe\ORM\ValidationResult;
36
use LeKoala\Admini\Traits\JsonResponse;
37
use SilverStripe\ORM\FieldType\DBField;
38
use SilverStripe\SiteConfig\SiteConfig;
39
use SilverStripe\Core\Injector\Injector;
40
use SilverStripe\Security\SecurityToken;
41
use SilverStripe\ORM\Hierarchy\Hierarchy;
42
use SilverStripe\ORM\ValidationException;
43
use SilverStripe\ORM\FieldType\DBHTMLText;
44
use SilverStripe\Control\ContentNegotiator;
45
use SilverStripe\Core\Manifest\ModuleLoader;
46
use SilverStripe\Security\PermissionProvider;
47
use SilverStripe\Core\Manifest\VersionProvider;
48
use SilverStripe\Forms\PrintableTransformation;
49
use SilverStripe\Control\HTTPResponse_Exception;
50
use SilverStripe\Forms\HTMLEditor\TinyMCEConfig;
51
use SilverStripe\Forms\HTMLEditor\HTMLEditorConfig;
52
use SilverStripe\Control\Middleware\HTTPCacheControlMiddleware;
53
54
/**
55
 * LeftAndMain is the parent class of all the two-pane views in the CMS.
56
 * If you are wanting to add more areas to the CMS, you can do it by subclassing LeftAndMain.
57
 *
58
 * This is essentially an abstract class which should be subclassed.
59
 *
60
 * @method alternateMenuDisplayCheck
61
 * @method alternateAccessCheck
62
 */
63
class LeftAndMain extends Controller implements PermissionProvider
64
{
65
    use JsonResponse;
66
    use Toasts;
1 ignored issue
show
introduced by
The trait LeKoala\Admini\Traits\Toasts requires some properties which are not provided by LeKoala\Admini\LeftAndMain: $Message, $ThemeColor
Loading history...
67
68
    /**
69
     * The current url segment attached to the LeftAndMain instance
70
     *
71
     * @config
72
     * @var string
73
     */
74
    private static $url_segment = null;
1 ignored issue
show
introduced by
The private property $url_segment is not used, and could be removed.
Loading history...
75
76
    /**
77
     * @config
78
     * @var string Used by {@link AdminiRootController} to augment Director route rules for sub-classes of LeftAndMain
79
     */
80
    private static $url_rule = '/$Action/$ID/$OtherID';
1 ignored issue
show
introduced by
The private property $url_rule is not used, and could be removed.
Loading history...
81
82
    /**
83
     * @config
84
     * @var string
85
     */
86
    private static $menu_title;
1 ignored issue
show
introduced by
The private property $menu_title is not used, and could be removed.
Loading history...
87
88
    /**
89
     * An icon name for last-icon. You can check MaterialIcons class for values
90
     * @config
91
     * @var string
92
     */
93
    private static $menu_icon;
0 ignored issues
show
introduced by
The private property $menu_icon is not used, and could be removed.
Loading history...
94
95
    /**
96
     * @config
97
     * @var int
98
     */
99
    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...
100
101
    /**
102
     * @config
103
     * @var int
104
     */
105
    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...
106
107
    /**
108
     * A subclass of {@link DataObject}.
109
     *
110
     * Determines what is managed in this interface, through
111
     * {@link getEditForm()} and other logic.
112
     *
113
     * @config
114
     * @var string
115
     */
116
    private static $tree_class = null;
1 ignored issue
show
introduced by
The private property $tree_class is not used, and could be removed.
Loading history...
117
118
    /**
119
     * @var array
120
     */
121
    private static $allowed_actions = [
1 ignored issue
show
introduced by
The private property $allowed_actions is not used, and could be removed.
Loading history...
122
        'index',
123
        'save',
124
        'printable',
125
        'show',
126
        'EditForm',
127
    ];
128
129
    /**
130
     * Current pageID for this request
131
     *
132
     * @var null
133
     */
134
    protected $pageID = null;
135
136
    /**
137
     * Assign themes to use for cms
138
     *
139
     * @config
140
     * @var array
141
     */
142
    private static $admin_themes = [
0 ignored issues
show
introduced by
The private property $admin_themes is not used, and could be removed.
Loading history...
143
        'lekoala/silverstripe-admini:forms',
144
        SSViewer::DEFAULT_THEME,
145
    ];
146
147
    /**
148
     * Codes which are required from the current user to view this controller.
149
     * If multiple codes are provided, all of them are required.
150
     * All CMS controllers require "CMS_ACCESS_LeftAndMain" as a baseline check,
151
     * and fall back to "CMS_ACCESS_<class>" if no permissions are defined here.
152
     * See {@link canView()} for more details on permission checks.
153
     *
154
     * @config
155
     * @var array
156
     */
157
    private static $required_permission_codes;
1 ignored issue
show
introduced by
The private property $required_permission_codes is not used, and could be removed.
Loading history...
158
159
    /**
160
     * Namespace for session info, e.g. current record.
161
     * Defaults to the current class name, but can be amended to share a namespace in case
162
     * controllers are logically bundled together, and mainly separated
163
     * to achieve more flexible templating.
164
     *
165
     * @config
166
     * @var string
167
     */
168
    private static $session_namespace;
0 ignored issues
show
introduced by
The private property $session_namespace is not used, and could be removed.
Loading history...
169
170
    /**
171
     * Register additional requirements through the {@link Requirements} class.
172
     * Used mainly to work around the missing "lazy loading" functionality
173
     * for getting css/javascript required after an ajax-call (e.g. loading the editform).
174
     *
175
     * YAML configuration example:
176
     * <code>
177
     * LeftAndMain:
178
     *   extra_requirements_javascript:
179
     *     - mysite/javascript/myscript.js
180
     * </code>
181
     *
182
     * @config
183
     * @var array
184
     */
185
    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...
186
187
    /**
188
     * YAML configuration example:
189
     * <code>
190
     * LeftAndMain:
191
     *   extra_requirements_css:
192
     *     mysite/css/mystyle.css:
193
     *       media: screen
194
     * </code>
195
     *
196
     * @config
197
     * @var array See {@link extra_requirements_javascript}
198
     */
199
    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...
200
201
    /**
202
     * @config
203
     * @var array See {@link extra_requirements_javascript}
204
     */
205
    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...
206
207
    /**
208
     * If true, call a keepalive ping every 5 minutes from the CMS interface,
209
     * to ensure that the session never dies.
210
     *
211
     * @config
212
     * @var bool
213
     */
214
    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...
215
216
    /**
217
     * Value of X-Frame-Options header
218
     *
219
     * @config
220
     * @var string
221
     */
222
    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...
223
224
    /**
225
     * The configuration passed to the supporting JS for each CMS section includes a 'name' key
226
     * that by default matches the FQCN of the current class. This setting allows you to change
227
     * the key if necessary (for example, if you are overloading CMSMain or another core class
228
     * and want to keep the core JS - which depends on the core class names - functioning, you
229
     * would need to set this to the FQCN of the class you are overloading).
230
     *
231
     * @config
232
     * @var string|null
233
     */
234
    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...
235
236
    /**
237
     * @var VersionProvider
238
     */
239
    protected $versionProvider;
240
241
    /**
242
     * @var array
243
     * @config
244
     */
245
    private static $casting = [
0 ignored issues
show
introduced by
The private property $casting is not used, and could be removed.
Loading history...
246
        'MainSvgIcon' => 'HTMLText'
247
    ];
248
249
    /**
250
     * The urls used for the links in the Help dropdown in the backend
251
     *
252
     * @config
253
     * @var array
254
     */
255
    private static $help_links = [
0 ignored issues
show
introduced by
The private property $help_links is not used, and could be removed.
Loading history...
256
        'CMS User help' => 'https://userhelp.silverstripe.org/en/4',
257
        'Developer docs' => 'https://docs.silverstripe.org/en/4/',
258
        'Community' => 'https://www.silverstripe.org/',
259
        'Feedback' => 'https://www.silverstripe.org/give-feedback/',
260
    ];
261
262
    /**
263
     * The href for the anchor on the Silverstripe logo
264
     *
265
     * @config
266
     * @var string
267
     */
268
    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...
269
270
    /**
271
     * The application name
272
     *
273
     * @config
274
     * @var string
275
     */
276
    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...
277
278
    /**
279
     * @param Member $member
280
     * @return bool
281
     */
282
    public function canView($member = null)
283
    {
284
        if (!$member && $member !== false) {
0 ignored issues
show
introduced by
The condition $member !== false is always false.
Loading history...
285
            $member = Security::getCurrentUser();
286
        }
287
288
        // cms menus only for logged-in members
289
        if (!$member) {
290
            return false;
291
        }
292
293
        // alternative extended checks
294
        if ($this->hasMethod('alternateAccessCheck')) {
295
            $alternateAllowed = $this->alternateAccessCheck($member);
0 ignored issues
show
Unused Code introduced by
The call to LeKoala\Admini\LeftAndMain::alternateAccessCheck() has too many arguments starting with $member. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

295
            /** @scrutinizer ignore-call */ 
296
            $alternateAllowed = $this->alternateAccessCheck($member);

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
296
            if ($alternateAllowed === false) {
297
                return false;
298
            }
299
        }
300
301
        // Check for "CMS admin" permission
302
        if (Permission::checkMember($member, "CMS_ACCESS_LeftAndMain")) {
303
            return true;
304
        }
305
306
        // Check for LeftAndMain sub-class permissions
307
        $codes = $this->getRequiredPermissions();
308
        if ($codes === false) { // allow explicit FALSE to disable subclass check
309
            return true;
310
        }
311
        foreach ((array)$codes as $code) {
312
            if (!Permission::checkMember($member, $code)) {
313
                return false;
314
            }
315
        }
316
317
        return true;
318
    }
319
320
    /**
321
     * Get list of required permissions
322
     *
323
     * @return array|string|bool Code, array of codes, or false if no permission required
324
     */
325
    public static function getRequiredPermissions()
326
    {
327
        $class = get_called_class();
328
        // If the user is accessing LeftAndMain directly, only generic permissions are required.
329
        if ($class === self::class) {
330
            return 'CMS_ACCESS';
331
        }
332
        $code = Config::inst()->get($class, 'required_permission_codes');
333
        if ($code === false) {
334
            return false;
335
        }
336
        if ($code) {
337
            return $code;
338
        }
339
        return 'CMS_ACCESS_' . $class;
340
    }
341
342
    protected function includeGoogleFont()
343
    {
344
        $font = self::config()->google_font;
345
        if (!$font) {
346
            return;
347
        }
348
        $preconnect = <<<HTML
349
<link rel="preconnect" href="https://fonts.googleapis.com" />
350
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
351
HTML;
352
        Requirements::insertHeadTags($preconnect, "googlefontspreconnect");
353
        Requirements::css("https://fonts.googleapis.com/css2?$font");
354
    }
355
356
    protected function includeLastIcon()
357
    {
358
        $preconnect = <<<HTML
359
<link rel="preconnect" href="https://fonts.googleapis.com" />
360
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
361
HTML;
362
        Requirements::insertHeadTags($preconnect, "googlefontspreconnect");
363
364
        // Could also host locally https://marella.me/material-icons/demo/#two-tone
365
        Requirements::css("https://fonts.googleapis.com/icon?family=Material+Icons+Two+Tone");
366
        Requirements::javascript('lekoala/silverstripe-admini: client/js/last-icon.min.js', ["type" => "application/javascript"]);
367
        $nonce = CspProvider::getCspNonce();
368
        $lastIconScript = <<<JS
369
<script nonce="$nonce">
370
    window.LastIcon = {
371
            types: {
372
            material: "twotone",
373
            },
374
            defaultSet: "material",
375
            fonts: ["material"],
376
        };
377
</script>
378
JS;
379
        Requirements::insertHeadTags($lastIconScript, __FUNCTION__);
380
    }
381
382
    protected function includeFavicon()
383
    {
384
        $icon = $this->MainSvgIcon();
385
        $encodedIcon = str_replace(['"', '#'], ['%22', '%23'], $icon);
386
        $html = <<<HTML
387
<link
388
rel="icon"
389
type="image/svg+xml"
390
href="data:image/svg+xml,$encodedIcon"
391
/>
392
HTML;
393
        Requirements::insertHeadTags($html, __FUNCTION__);
394
    }
395
396
    /**
397
     * @return int
398
     */
399
    public function SessionKeepAlivePing()
400
    {
401
        return LeftAndMain::config()->uninherited('session_keepalive_ping');
402
    }
403
404
    /**
405
     * The icon to be used either as favicon or in the menu
406
     */
407
    public function MainSvgIcon(): string
408
    {
409
        $emoji = self::config()->svg_emoji ?? '💠';
410
        $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>';
411
        return $icon;
412
    }
413
414
    public function AdminDir(): string
415
    {
416
        $path = "js/admini.js";
417
        $resource = ModuleLoader::getModule('lekoala/silverstripe-base')->getResource($path);
418
        $dir = dirname($resource->getRelativePath());
419
        return $dir;
420
    }
421
422
    /**
423
     * Preload fonts
424
     */
425
    public function PreloadFonts()
426
    {
427
        $fonts = self::config()->preload_fonts;
428
        if (empty($fonts)) {
429
            return;
430
        }
431
        $dir = $this->AdminDir();
432
433
        $html = '';
434
        if (!empty($fonts)) {
435
            foreach ($fonts as $font) {
436
                $font = $dir . $font;
437
                // browsers will ignore preloaded fonts without the crossorigin attribute, which will cause the browser to actually fetch the font twice
438
                $html .= "<link rel=\"preload\" href=\"$font\" as=\"font\" type=\"font/woff2\" crossOrigin=\"anonymous\" >\n";
439
            }
440
        }
441
        Requirements::insertHeadTags($html, __FUNCTION__);
442
    }
443
444
    public function HasMinimenu(): bool
445
    {
446
        return (bool)Cookie::get("minimenu");
447
    }
448
449
    /**
450
     * In the CMS, we use tabs in the header
451
     * Do not render actual cms tabs
452
     *
453
     * @param Form $form
454
     * @return void
455
     */
456
    public function setCMSTabset(Form $form)
457
    {
458
        if ($form->Fields()->hasTabSet()) {
459
            $form->Fields()->findOrMakeTab('Root')->setTemplate('SilverStripe\\Forms\\CMSTabSet');
460
        }
461
    }
462
463
    /**
464
     * Check if the current request has a X-Formschema-Request header set.
465
     * Used by conditional logic that responds to validation results
466
     *
467
     * @return bool
468
     */
469
    protected function getSchemaRequested()
470
    {
471
        return false;
472
    }
473
474
    protected function loadExtraRequirements()
475
    {
476
        $extraJs = $this->config()->get('extra_requirements_javascript');
477
        if ($extraJs) {
478
            foreach ($extraJs as $file => $config) {
479
                if (is_numeric($file)) {
480
                    $file = $config;
481
                }
482
483
                Requirements::javascript($file);
484
            }
485
        }
486
487
        $extraCss = $this->config()->get('extra_requirements_css');
488
        if ($extraCss) {
489
            foreach ($extraCss as $file => $config) {
490
                if (is_numeric($file)) {
491
                    $file = $config;
492
                    $config = array();
493
                }
494
495
                Requirements::css($file, isset($config['media']) ? $config['media'] : null);
496
            }
497
        }
498
499
        $extraThemedCss = $this->config()->get('extra_requirements_themedCss');
500
        if ($extraThemedCss) {
501
            foreach ($extraThemedCss as $file => $config) {
502
                if (is_numeric($file)) {
503
                    $file = $config;
504
                    $config = array();
505
                }
506
507
                Requirements::themedCSS($file, isset($config['media']) ? $config['media'] : null);
508
            }
509
        }
510
    }
511
512
    /**
513
     * @uses LeftAndMainExtension->init()
514
     * @uses LeftAndMainExtension->accessedCMS()
515
     * @uses CMSMenu
516
     */
517
    protected function init()
518
    {
519
        parent::init();
520
521
        // SSViewer::config()->source_file_comments = true;
522
523
        // Replace GridField
524
        Injector::inst()->registerService(new TabulatorGrid(""), \SilverStripe\Forms\GridField\GridField::class);
525
526
        DeferBackend::config()->enable_js_modules = true;
527
        DeferBackend::replaceBackend();
528
529
        $this->showToasterMessage();
530
531
        HTTPCacheControlMiddleware::singleton()->disableCache();
532
533
        SSViewer::setRewriteHashLinksDefault(false);
534
        ContentNegotiator::setEnabled(false);
535
536
        // set language based on current user locale
537
        $member = Security::getCurrentUser();
538
        if (!empty($member->Locale)) {
539
            i18n::set_locale($member->Locale);
540
        }
541
542
        // Allow customisation of the access check by a extension
543
        // Also all the canView() check to execute Controller::redirect()
544
        if (!$this->canView() && !$this->getResponse()->isFinished()) {
545
            // When access /admin/, we should try a redirect to another part of the admin rather than be locked out
546
            $menu = $this->MainMenu();
547
            foreach ($menu as $candidate) {
548
                $canView = $candidate->Link &&
549
                    $candidate->Link != $this->Link()
550
                    && $candidate->MenuItem->controller
551
                    && singleton($candidate->MenuItem->controller)->canView();
552
                if ($canView) {
553
                    $this->redirect($candidate->Link);
554
                    return;
555
                }
556
            }
557
558
            if (Security::getCurrentUser()) {
559
                $this->getRequest()->getSession()->clear("BackURL");
560
            }
561
562
            // if no alternate menu items have matched, return a permission error
563
            $messageSet = array(
564
                'default' => _t(
565
                    __CLASS__ . '.PERMDEFAULT',
566
                    "You must be logged in to access the administration area; please enter your credentials below."
567
                ),
568
                'alreadyLoggedIn' => _t(
569
                    __CLASS__ . '.PERMALREADY',
570
                    "I'm sorry, but you can't access that part of the CMS.  If you want to log in as someone else, do"
571
                        . " so below."
572
                ),
573
                'logInAgain' => _t(
574
                    __CLASS__ . '.PERMAGAIN',
575
                    "You have been logged out of the CMS.  If you would like to log in again, enter a username and"
576
                        . " password below."
577
                ),
578
            );
579
580
            Security::permissionFailure($this, $messageSet);
581
            return;
582
        }
583
584
        // Don't continue if there's already been a redirection request.
585
        if ($this->redirectedTo()) {
586
            return;
587
        }
588
589
        // Audit logging hook
590
        if (empty($_REQUEST['executeForm']) && !$this->getRequest()->isAjax()) {
591
            $this->extend('accessedCMS');
592
        }
593
594
        $this->includeFavicon();
595
        $this->includeLastIcon();
596
        $this->includeGoogleFont();
597
598
        Requirements::javascript('lekoala/silverstripe-admini: client/js/admini.min.js');
599
        // This must be applied last, so we put it at the bottom manually because requirements backend may inject stuff in the body
600
        // Requirements::customScript("window.admini.init()");
601
        Requirements::css('lekoala/silverstripe-admini: client/css/admini.min.css');
602
        Requirements::css('lekoala/silverstripe-admini: client/css/custom.css');
603
604
        //TODO: restore these features
605
        // Requirements::add_i18n_javascript('silverstripe/admin:client/lang');
606
        // Requirements::add_i18n_javascript('silverstripe/admin:client/dist/moment-locales', false, false, true);
607
608
        // if (LeftAndMain::config()->uninherited('session_keepalive_ping')) {
609
        //     Requirements::javascript('silverstripe/admin: client/dist/js/LeftAndMain.Ping.js');
610
        // }
611
612
        $this->loadExtraRequirements();
613
        $this->extend('init');
614
615
        // Assign default cms theme and replace user-specified themes
616
        // This allows us for instance to set custom form templates for BS5
617
        SSViewer::set_themes(LeftAndMain::config()->uninherited('admin_themes'));
618
619
        // Versioned support
620
        if (class_exists(Versioned::class)) {
621
            // Set the current reading mode
622
            Versioned::set_stage(Versioned::DRAFT);
623
624
            // Set default reading mode to suppress ?stage=Stage querystring params in CMS
625
            Versioned::set_default_reading_mode(Versioned::get_reading_mode());
626
        }
627
    }
628
629
    public function handleRequest(HTTPRequest $request)
630
    {
631
        try {
632
            $response = parent::handleRequest($request);
633
        } catch (ValidationException $e) {
634
            // Nicer presentation of model-level validation errors
635
            $msgs = _t(__CLASS__ . '.ValidationError', 'Validation error') . ': '
636
                . $e->getMessage();
637
            $this->sessionMessage($msgs, "bad");
638
            return $this->redirectBack();
639
        }
640
641
        $title = $this->Title();
642
643
        //TODO: check this when implementing ajax
644
        if (!$response->getHeader('X-Controller')) {
645
            $response->addHeader('X-Controller', static::class);
646
        }
647
        if (!$response->getHeader('X-Title')) {
648
            $response->addHeader('X-Title', urlencode($title));
649
        }
650
651
        // Prevent clickjacking, see https://developer.mozilla.org/en-US/docs/HTTP/X-Frame-Options
652
        $originalResponse = $this->getResponse();
653
        $originalResponse->addHeader('X-Frame-Options', LeftAndMain::config()->uninherited('frame_options'));
654
        $originalResponse->addHeader('Vary', 'X-Requested-With');
655
656
        return $response;
657
    }
658
659
    /**
660
     * Overloaded redirection logic to trigger a fake redirect on ajax requests.
661
     * While this violates HTTP principles, its the only way to work around the
662
     * fact that browsers handle HTTP redirects opaquely, no intervention via JS is possible.
663
     * In isolation, that's not a problem - but combined with history.pushState()
664
     * it means we would request the same redirection URL twice if we want to update the URL as well.
665
     * See LeftAndMain.js for the required jQuery ajaxComplete handlers.
666
     *
667
     * @param string $url
668
     * @param int $code
669
     * @return HTTPResponse|string
670
     */
671
    public function redirect($url, $code = 302)
672
    {
673
        //TODO: check this when implementing ajax navigation
674
        if ($this->getRequest()->isAjax()) {
675
            $response = $this->getResponse();
676
            $response->addHeader('X-ControllerURL', $url);
677
            if ($this->getRequest()->getHeader('X-Pjax') && !$response->getHeader('X-Pjax')) {
678
                $response->addHeader('X-Pjax', $this->getRequest()->getHeader('X-Pjax'));
679
            }
680
            $newResponse = new HTTPResponse(
681
                $response->getBody(),
682
                $response->getStatusCode(),
683
                $response->getStatusDescription()
684
            );
685
            foreach ($response->getHeaders() as $k => $v) {
686
                $newResponse->addHeader($k, $v);
687
            }
688
689
            // $newResponse->setIsFinished(true);
690
            // $this->setResponse($newResponse);
691
692
            return ''; // Actual response will be re-requested by client
693
        } else {
694
            return parent::redirect($url, $code);
695
        }
696
    }
697
698
    /**
699
     * @param HTTPRequest $request
700
     * @return HTTPResponse|SilverStripe\ORM\FieldType\DBHTMLText
0 ignored issues
show
Bug introduced by
The type LeKoala\Admini\SilverStr...RM\FieldType\DBHTMLText was not found. Did you mean SilverStripe\ORM\FieldType\DBHTMLText? If so, make sure to prefix the type with \.
Loading history...
701
     */
702
    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

702
    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...
703
    {
704
        return $this->renderWith($this->getViewer('show'));
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->renderWith...his->getViewer('show')) returns the type SilverStripe\ORM\FieldType\DBHTMLText which is incompatible with the documented return type LeKoala\Admini\SilverStr...pe\Control\HTTPResponse.
Loading history...
705
    }
706
707
    /**
708
     * You should implement a Link() function in your subclass of LeftAndMain,
709
     * to point to the URL of that particular controller.
710
     *
711
     * @param string $action
712
     * @return string
713
     */
714
    public function Link($action = null)
715
    {
716
        // LeftAndMain methods have a top-level uri access
717
        if (static::class === LeftAndMain::class) {
0 ignored issues
show
introduced by
The condition static::class === LeKoal...mini\LeftAndMain::class is always true.
Loading history...
718
            $segment = '';
719
        } else {
720
            // Get url_segment
721
            $segment = $this->config()->get('url_segment');
722
            if (!$segment) {
723
                throw new BadMethodCallException(
724
                    sprintf('LeftAndMain subclasses (%s) must have url_segment', static::class)
725
                );
726
            }
727
        }
728
729
        $link = Controller::join_links(
730
            AdminiRootController::admin_url(),
731
            $segment,
732
            '/', // trailing slash needed if $action is null!
733
            "$action"
734
        );
735
        $this->extend('updateLink', $link);
736
        return $link;
737
    }
738
739
    /**
740
     * Get menu title for this section (translated)
741
     *
742
     * @param string $class Optional class name if called on LeftAndMain directly
743
     * @param bool $localise Determine if menu title should be localised via i18n.
744
     * @return string Menu title for the given class
745
     */
746
    public static function menu_title($class = null, $localise = true)
747
    {
748
        if ($class && is_subclass_of($class, __CLASS__)) {
749
            // Respect oveloading of menu_title() in subclasses
750
            return $class::menu_title(null, $localise);
751
        }
752
        if (!$class) {
753
            $class = get_called_class();
754
        }
755
756
        // Get default class title
757
        $title = static::config()->get('menu_title');
758
        if (!$title) {
759
            $title = preg_replace('/Admin$/', '', $class);
760
        }
761
762
        // Check localisation
763
        if (!$localise) {
764
            return $title;
765
        }
766
        return i18n::_t("{$class}.MENUTITLE", $title);
767
    }
768
769
    /**
770
     * Return the name for the menu icon
771
     * @param string $class
772
     * @return string
773
     */
774
    public static function menu_icon_for_class($class)
775
    {
776
        return Config::inst()->get($class, 'menu_icon');
777
    }
778
779
    /**
780
     * @param HTTPRequest $request
781
     * @return HTTPResponse|SilverStripe\ORM\FieldType\DBHTMLText
782
     * @throws HTTPResponse_Exception
783
     */
784
    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

784
    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...
785
    {
786
        // TODO Necessary for TableListField URLs to work properly
787
        // TODO: check why this is needed
788
        // if ($request->param('ID')) {
789
        //     $this->setCurrentPageID($request->param('ID'));
790
        // }
791
        return $this->renderWith($this->getViewer('show'));
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->renderWith...his->getViewer('show')) returns the type SilverStripe\ORM\FieldType\DBHTMLText which is incompatible with the documented return type LeKoala\Admini\SilverStr...pe\Control\HTTPResponse.
Loading history...
792
    }
793
794
    //------------------------------------------------------------------------------------------//
795
    // Main UI components
796
797
    /**
798
     * Returns the main menu of the CMS.  This is also used by init()
799
     * to work out which sections the user has access to.
800
     *
801
     * @param bool $cached
802
     * @return SS_List
803
     */
804
    public function MainMenu($cached = true)
805
    {
806
        static $menuCache = null;
807
        if ($menuCache === null || !$cached) {
808
            // Don't accidentally return a menu if you're not logged in - it's used to determine access.
809
            if (!Security::getCurrentUser()) {
810
                return new ArrayList();
811
            }
812
813
            // Encode into DO set
814
            $menu = new ArrayList();
815
            $menuItems = CMSMenu::get_viewable_menu_items();
816
817
            // extra styling for custom menu-icons
818
            $menuIconStyling = '';
819
820
            if ($menuItems) {
821
                /** @var CMSMenuItem $menuItem */
822
                foreach ($menuItems as $code => $menuItem) {
823
                    // alternate permission checks (in addition to LeftAndMain->canView())
824
                    $alternateCheck = isset($menuItem->controller)
825
                        && $this->hasMethod('alternateMenuDisplayCheck')
826
                        && !$this->alternateMenuDisplayCheck($menuItem->controller);
0 ignored issues
show
Unused Code introduced by
The call to LeKoala\Admini\LeftAndMa...rnateMenuDisplayCheck() has too many arguments starting with $menuItem->controller. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

826
                        && !$this->/** @scrutinizer ignore-call */ alternateMenuDisplayCheck($menuItem->controller);

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
827
                    if ($alternateCheck) {
828
                        continue;
829
                    }
830
831
                    // linking mode
832
                    $linkingmode = "link";
833
                    if ($menuItem->controller && get_class($this) == $menuItem->controller) {
834
                        $linkingmode = "current";
835
                    } elseif (strpos($this->Link(), $menuItem->url) !== false) {
836
                        if ($this->Link() == $menuItem->url) {
837
                            $linkingmode = "current";
838
839
                            // default menu is the one with a blank {@link url_segment}
840
                        } elseif (singleton($menuItem->controller)->config()->get('url_segment') == '') {
841
                            if ($this->Link() == AdminiRootController::admin_url()) {
842
                                $linkingmode = "current";
843
                            }
844
                        } else {
845
                            $linkingmode = "current";
846
                        }
847
                    }
848
849
                    // already set in CMSMenu::populate_menu(), but from a static pre-controller
850
                    // context, so doesn't respect the current user locale in _t() calls - as a workaround,
851
                    // we simply call LeftAndMain::menu_title() again
852
                    // if we're dealing with a controller
853
                    if ($menuItem->controller) {
854
                        $title = LeftAndMain::menu_title($menuItem->controller);
855
                    } else {
856
                        $title = $menuItem->title;
857
                    }
858
859
                    // Provide styling for custom $menu-icon. Done here instead of in
860
                    // CMSMenu::populate_menu(), because the icon is part of
861
                    // the CMS right pane for the specified class as well...
862
                    $IconName = '';
863
                    if ($menuItem->controller) {
864
                        $IconName = LeftAndMain::menu_icon_for_class($menuItem->controller);
865
                    } else {
866
                        $IconName = $menuItem->iconName;
867
                    }
868
                    if (!$IconName) {
869
                        $IconName = "arrow_right";
870
                    }
871
                    $menuItem->addExtraClass("sidebar-link");
872
873
                    $menu->push(new ArrayData([
874
                        "MenuItem" => $menuItem,
875
                        "AttributesHTML" => $menuItem->getAttributesHTML(),
876
                        "Title" => $title,
877
                        "Code" => $code,
878
                        "IconName" => $IconName,
879
                        "Link" => $menuItem->url,
880
                        "LinkingMode" => $linkingmode
881
                    ]));
882
                }
883
            }
884
            if ($menuIconStyling) {
885
                Requirements::customCSS($menuIconStyling);
886
            }
887
888
            $menuCache = $menu;
889
        }
890
891
        return $menuCache;
892
    }
893
894
    public function Menu()
895
    {
896
        return $this->renderWith($this->getTemplatesWithSuffix('_Menu'));
897
    }
898
899
    /**
900
     * @todo Wrap in CMSMenu instance accessor
901
     * @return ArrayData A single menu entry (see {@link MainMenu})
902
     */
903
    public function MenuCurrentItem()
904
    {
905
        $items = $this->MainMenu();
906
        return $items->find('LinkingMode', 'current');
907
    }
908
909
    /**
910
     * Return appropriate template(s) for this class, with the given suffix using
911
     * {@link SSViewer::get_templates_by_class()}
912
     *
913
     * @param string $suffix
914
     * @return string|array
915
     */
916
    public function getTemplatesWithSuffix($suffix)
917
    {
918
        $templates = SSViewer::get_templates_by_class(get_class($this), $suffix, __CLASS__);
919
        return SSViewer::chooseTemplate($templates);
920
    }
921
922
    public function Content()
923
    {
924
        return $this->renderWith($this->getTemplatesWithSuffix('_Content'));
925
    }
926
927
    /**
928
     * Get dataobject from the current ID
929
     *
930
     * @param int|DataObject $id ID or object
931
     * @return DataObject
932
     */
933
    public function getRecord($id)
934
    {
935
        $className = $this->config()->get('tree_class');
936
        if (!$className) {
937
            return null;
938
        }
939
        if ($id instanceof $className) {
940
            /** @var DataObject $id */
941
            return $id;
942
        }
943
        if ($id === 'root') {
0 ignored issues
show
introduced by
The condition $id === 'root' is always false.
Loading history...
944
            return DataObject::singleton($className);
945
        }
946
        if (is_numeric($id)) {
947
            return DataObject::get_by_id($className, $id);
948
        }
949
        return null;
950
    }
951
952
    /**
953
     * Called by CMSBreadcrumbs.ss
954
     * @param bool $unlinked
955
     * @return ArrayList
956
     */
957
    public function Breadcrumbs($unlinked = false)
958
    {
959
        $items = new ArrayList(array(
960
            new ArrayData(array(
961
                'Title' => $this->menu_title(),
962
                'Link' => ($unlinked) ? false : $this->Link()
963
            ))
964
        ));
965
966
        return $items;
967
    }
968
969
    /**
970
     * Save  handler
971
     *
972
     * @param array $data
973
     * @param Form $form
974
     * @return HTTPResponse
975
     */
976
    public function save($data, $form)
977
    {
978
        $request = $this->getRequest();
0 ignored issues
show
Unused Code introduced by
The assignment to $request is dead and can be removed.
Loading history...
979
        $className = $this->config()->get('tree_class');
980
981
        // Existing or new record?
982
        $id = $data['ID'];
983
        if (is_numeric($id) && $id > 0) {
984
            $record = DataObject::get_by_id($className, $id);
985
            if ($record && !$record->canEdit()) {
986
                return Security::permissionFailure($this);
987
            }
988
            if (!$record || !$record->ID) {
0 ignored issues
show
introduced by
$record is of type SilverStripe\ORM\DataObject, thus it always evaluated to true.
Loading history...
989
                $this->httpError(404, "Bad record ID #" . (int)$id);
990
            }
991
        } else {
992
            if (!singleton($this->config()->get('tree_class'))->canCreate()) {
993
                return Security::permissionFailure($this);
994
            }
995
            $record = $this->getNewItem($id, false);
996
        }
997
998
        // save form data into record
999
        $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

999
        $form->saveInto($record, /** @scrutinizer ignore-type */ true);
Loading history...
1000
        $record->write();
1001
        $this->extend('onAfterSave', $record);
1002
        $this->setCurrentPageID($record->ID);
0 ignored issues
show
Bug introduced by
The method setCurrentPageID() does not exist on LeKoala\Admini\LeftAndMain. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

1002
        $this->/** @scrutinizer ignore-call */ 
1003
               setCurrentPageID($record->ID);
Loading history...
1003
1004
        $message = _t(__CLASS__ . '.SAVEDUP', 'Saved.');
1005
        $this->sessionMessage($message, "good");
1006
        return $this->redirectBack();
1007
    }
1008
1009
    /**
1010
     * Create new item.
1011
     *
1012
     * @param string|int $id
1013
     * @param bool $setID
1014
     * @return DataObject
1015
     */
1016
    public function getNewItem($id, $setID = true)
1017
    {
1018
        $class = $this->config()->get('tree_class');
1019
        $object = Injector::inst()->create($class);
1020
        if ($setID) {
1021
            $object->ID = $id;
1022
        }
1023
        return $object;
1024
    }
1025
1026
    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

1026
    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...
1027
    {
1028
        $className = $this->config()->get('tree_class');
1029
1030
        $id = $data['ID'];
1031
        $record = DataObject::get_by_id($className, $id);
1032
        if ($record && !$record->canDelete()) {
1033
            return Security::permissionFailure();
1034
        }
1035
        if (!$record || !$record->ID) {
0 ignored issues
show
introduced by
$record is of type SilverStripe\ORM\DataObject, thus it always evaluated to true.
Loading history...
1036
            $this->httpError(404, "Bad record ID #" . (int)$id);
1037
        }
1038
1039
        $record->delete();
1040
        $this->sessionMessage(_t(__CLASS__ . '.DELETED', 'Deleted.'));
1041
        return $this->redirectBack();
1042
    }
1043
1044
    /**
1045
     * Retrieves an edit form, either for display, or to process submitted data.
1046
     * Also used in the template rendered through {@link Right()} in the $EditForm placeholder.
1047
     *
1048
     * This is a "pseudo-abstract" methoed, usually connected to a {@link getEditForm()}
1049
     * method in an entwine subclass. This method can accept a record identifier,
1050
     * selected either in custom logic, or through {@link currentPageID()}.
1051
     * The form usually construct itself from {@link DataObject->getCMSFields()}
1052
     * for the specific managed subclass defined in {@link LeftAndMain::$tree_class}.
1053
     *
1054
     * @param HTTPRequest $request Passed if executing a HTTPRequest directly on the form.
1055
     * If empty, this is invoked as $EditForm in the template
1056
     * @return Form Should return a form regardless wether a record has been found.
1057
     *  Form might be readonly if the current user doesn't have the permission to edit
1058
     *  the record.
1059
     */
1060
    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

1060
    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...
1061
    {
1062
        return $this->getEditForm();
1063
    }
1064
1065
    /**
1066
     * Calls {@link SiteTree->getCMSFields()} by default to determine the form fields to display.
1067
     *
1068
     * @param int $id
1069
     * @param FieldList $fields
1070
     * @return Form
1071
     */
1072
    public function getEditForm($id = null, $fields = null)
1073
    {
1074
        if (!$id) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $id of type integer|null is loosely compared to false; this is ambiguous if the integer can be 0. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

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