Passed
Push — master ( 9ee6e4...a5bb89 )
by Thomas
02:28
created

LeftAndMain::LinkHash()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 12
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

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

737
    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...
738
    {
739
        return $this->renderWith($this->getViewer('show'));
740
    }
741
742
    /**
743
     * You should implement a Link() function in your subclass of LeftAndMain,
744
     * to point to the URL of that particular controller.
745
     *
746
     * @param string $action
747
     * @return string
748
     */
749
    public function Link($action = null)
750
    {
751
        // LeftAndMain methods have a top-level uri access
752
        if (static::class === LeftAndMain::class) {
0 ignored issues
show
introduced by
The condition static::class === LeKoal...mini\LeftAndMain::class is always true.
Loading history...
753
            $segment = '';
754
        } else {
755
            // Get url_segment
756
            $segment = $this->config()->get('url_segment');
757
            if (!$segment) {
758
                throw new BadMethodCallException(
759
                    sprintf('LeftAndMain subclasses (%s) must have url_segment', static::class)
760
                );
761
            }
762
        }
763
764
        $link = Controller::join_links(
765
            AdminiRootController::admin_url(),
766
            $segment,
767
            '/', // trailing slash needed if $action is null!
768
            "$action"
769
        );
770
        $this->extend('updateLink', $link);
771
        return $link;
772
    }
773
774
    /**
775
     * Get menu title for this section (translated)
776
     *
777
     * @param string $class Optional class name if called on LeftAndMain directly
778
     * @param bool $localise Determine if menu title should be localised via i18n.
779
     * @return string Menu title for the given class
780
     */
781
    public static function menu_title($class = null, $localise = true)
782
    {
783
        if ($class && is_subclass_of($class, __CLASS__)) {
784
            // Respect oveloading of menu_title() in subclasses
785
            return $class::menu_title(null, $localise);
786
        }
787
        if (!$class) {
788
            $class = get_called_class();
789
        }
790
791
        // Get default class title
792
        $title = static::config()->get('menu_title');
793
        if (!$title) {
794
            $title = preg_replace('/Admin$/', '', $class);
795
        }
796
797
        // Check localisation
798
        if (!$localise) {
799
            return $title;
800
        }
801
        return i18n::_t("{$class}.MENUTITLE", $title);
802
    }
803
804
    /**
805
     * Return the name for the menu icon
806
     * @param string $class
807
     * @return string
808
     */
809
    public static function menu_icon_for_class($class)
810
    {
811
        return Config::inst()->get($class, 'menu_icon');
812
    }
813
814
    /**
815
     * @param HTTPRequest $request
816
     * @return HTTPResponse|DBHTMLText
817
     * @throws HTTPResponse_Exception
818
     */
819
    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

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

1034
        $form->saveInto($record, /** @scrutinizer ignore-type */ true);
Loading history...
1035
        $record->write();
1036
        $this->extend('onAfterSave', $record);
1037
1038
        //TODO: investigate if this is needed
1039
        // $this->setCurrentPageID($record->ID);
1040
1041
        $message = _t(__CLASS__ . '.SAVEDUP', 'Saved.');
1042
        $this->sessionMessage($message, "good");
1043
        return $this->redirectBack();
1044
    }
1045
1046
    /**
1047
     * Create new item.
1048
     *
1049
     * @param string|int $id
1050
     * @param bool $setID
1051
     * @return DataObject
1052
     */
1053
    public function getNewItem($id, $setID = true)
1054
    {
1055
        $class = $this->config()->get('tree_class');
1056
        $object = Injector::inst()->create($class);
1057
        if ($setID) {
1058
            $object->ID = $id;
1059
        }
1060
        return $object;
1061
    }
1062
1063
    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

1063
    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...
1064
    {
1065
        $className = $this->config()->get('tree_class');
1066
1067
        $id = $data['ID'];
1068
        $record = DataObject::get_by_id($className, $id);
1069
        if ($record && !$record->canDelete()) {
1070
            return Security::permissionFailure();
1071
        }
1072
        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...
1073
            $this->httpError(404, "Bad record ID #" . (int)$id);
1074
        }
1075
1076
        $record->delete();
1077
        $this->sessionMessage(_t(__CLASS__ . '.DELETED', 'Deleted.'));
1078
        return $this->redirectBack();
1079
    }
1080
1081
    /**
1082
     * Retrieves an edit form, either for display, or to process submitted data.
1083
     * Also used in the template rendered through {@link Right()} in the $EditForm placeholder.
1084
     *
1085
     * This is a "pseudo-abstract" methoed, usually connected to a {@link getEditForm()}
1086
     * method in an entwine subclass. This method can accept a record identifier,
1087
     * selected either in custom logic, or through {@link currentPageID()}.
1088
     * The form usually construct itself from {@link DataObject->getCMSFields()}
1089
     * for the specific managed subclass defined in {@link LeftAndMain::$tree_class}.
1090
     *
1091
     * @param HTTPRequest $request Passed if executing a HTTPRequest directly on the form.
1092
     * If empty, this is invoked as $EditForm in the template
1093
     * @return Form Should return a form regardless wether a record has been found.
1094
     *  Form might be readonly if the current user doesn't have the permission to edit
1095
     *  the record.
1096
     */
1097
    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

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