Passed
Push — v3 ( dc9fae...2f2fc6 )
by Andrew
18:54 queued 11:52
created

src/Retour.php (2 issues)

1
<?php
2
/**
3
 * Retour plugin for Craft CMS 3.x
4
 *
5
 * Retour allows you to intelligently redirect legacy URLs, so that you don't
6
 * lose SEO value when rebuilding & restructuring a website
7
 *
8
 * @link      https://nystudio107.com/
9
 * @copyright Copyright (c) 2018 nystudio107
10
 */
11
12
namespace nystudio107\retour;
13
14
use nystudio107\retour\assetbundles\retour\RetourAsset;
15
use nystudio107\retour\gql\interfaces\RetourInterface;
16
use nystudio107\retour\gql\queries\RetourQuery;
17
use nystudio107\retour\listeners\GetCraftQLSchema;
18
use nystudio107\retour\models\Settings;
19
use nystudio107\retour\services\Redirects;
20
use nystudio107\retour\services\Statistics;
21
use nystudio107\retour\variables\RetourVariable;
22
use nystudio107\retour\widgets\RetourWidget;
23
24
use nystudio107\pluginmanifest\services\ManifestService;
25
26
use Craft;
27
use craft\base\Element;
28
use craft\base\Plugin;
29
use craft\events\ElementEvent;
30
use craft\events\ExceptionEvent;
31
use craft\events\PluginEvent;
32
use craft\events\RegisterCacheOptionsEvent;
33
use craft\events\RegisterComponentTypesEvent;
34
use craft\events\RegisterGqlQueriesEvent;
35
use craft\events\RegisterGqlTypesEvent;
36
use craft\events\RegisterUrlRulesEvent;
37
use craft\events\RegisterUserPermissionsEvent;
38
use craft\helpers\ElementHelper;
39
use craft\helpers\UrlHelper;
40
use craft\services\Elements;
41
use craft\services\Dashboard;
42
use craft\services\Gql;
43
use craft\services\Plugins;
44
use craft\services\UserPermissions;
45
use craft\utilities\ClearCaches;
46
use craft\web\ErrorHandler;
47
use craft\web\twig\variables\CraftVariable;
48
use craft\web\UrlManager;
49
50
use yii\base\Event;
51
use yii\base\Exception;
52
use yii\web\HttpException;
53
54
use markhuot\CraftQL\Builders\Schema;
55
use markhuot\CraftQL\CraftQL;
56
use markhuot\CraftQL\Events\AlterSchemaFields;
57
58
/** @noinspection MissingPropertyAnnotationsInspection */
59
60
/**
61
 * Class Retour
62
 *
63
 * @author    nystudio107
64
 * @package   Retour
65
 * @since     3.0.0
66
 *
67
 * @property Redirects          $redirects
68
 * @property Statistics         $statistics
69
 * @property ManifestService    $manifest
70
 */
71
class Retour extends Plugin
72
{
73
    // Constants
74
    // =========================================================================
75
76
    const DEVMODE_CACHE_DURATION = 30;
77
78
    // Static Properties
79
    // =========================================================================
80
81
    /**
82
     * @var Retour
83
     */
84
    public static $plugin;
85
86
    /**
87
     * @var Settings
88
     */
89
    public static $settings;
90
91
    /**
92
     * @var int
93
     */
94
    public static $cacheDuration;
95
96
    /**
97
     * @var HttpException
98
     */
99
    public static $currentException;
100
101
    /**
102
     * @var bool
103
     */
104
    public static $craft31 = false;
105
106
    /**
107
     * @var bool
108
     */
109
    public static $craft32 = false;
110
111
    /**
112
     * @var bool
113
     */
114
    public static $craft33 = false;
115
116
    // Static Methods
117
    // =========================================================================
118
119
    /**
120
     * @inheritdoc
121
     */
122
    public function __construct($id, $parent = null, array $config = [])
123
    {
124
        $config['components'] = [
125
            'redirects' => Redirects::class,
126
            'statistics' => Statistics::class,
127
        ];
128
129
        parent::__construct($id, $parent, $config);
130
    }
131
132
    // Public Properties
133
    // =========================================================================
134
135
    /**
136
     * @var string
137
     */
138
    public $schemaVersion = '3.0.9';
139
140
    /**
141
     * @var bool
142
     */
143
    public $hasCpSection = true;
144
145
    /**
146
     * @var bool
147
     */
148
    public $hasCpSettings = true;
149
150
    /**
151
     * @var array The URIs for the element before it was saved
152
     */
153
    public $oldElementUris = [];
154
155
    // Public Methods
156
    // =========================================================================
157
158
    /**
159
     * @inheritdoc
160
     */
161
    public function init()
162
    {
163
        parent::init();
164
        self::$plugin = $this;
165
        // Initialize properties
166
        self::$settings = $this->getSettings();
167
        self::$craft31 = version_compare(Craft::$app->getVersion(), '3.1', '>=');
168
        self::$craft32 = version_compare(Craft::$app->getVersion(), '3.2', '>=');
169
        self::$craft33 = version_compare(Craft::$app->getVersion(), '3.3', '>=');
170
        $this->name = self::$settings->pluginName;
171
        self::$cacheDuration = Craft::$app->getConfig()->getGeneral()->devMode
172
            ? $this::DEVMODE_CACHE_DURATION
173
            : null;
174
        // Handle any console commands
175
        $request = Craft::$app->getRequest();
176
        if ($request->getIsConsoleRequest()) {
177
            $this->controllerNamespace = 'nystudio107\retour\console\controllers';
178
        }
179
        // Install our event listeners
180
        $this->installEventListeners();
181
        // Log that Retour has been loaded
182
        Craft::info(
183
            Craft::t(
184
                'retour',
185
                '{name} plugin loaded',
186
                ['name' => $this->name]
187
            ),
188
            __METHOD__
189
        );
190
    }
191
192
    /**
193
     * @inheritdoc
194
     */
195
    public function getSettingsResponse()
196
    {
197
        // Just redirect to the plugin settings page
198
        Craft::$app->getResponse()->redirect(UrlHelper::cpUrl('retour/settings'));
199
    }
200
201
    /**
202
     * @inheritdoc
203
     */
204
    public function getCpNavItem()
205
    {
206
        $subNavs = [];
207
        $navItem = parent::getCpNavItem();
208
        $currentUser = Craft::$app->getUser()->getIdentity();
209
        // Only show sub-navs the user has permission to view
210
        if ($currentUser->can('retour:dashboard')) {
211
            $subNavs['dashboard'] = [
212
                'label' => 'Dashboard',
213
                'url' => 'retour/dashboard',
214
            ];
215
        }
216
        if ($currentUser->can('retour:redirects')) {
217
            $subNavs['redirects'] = [
218
                'label' => 'Redirects',
219
                'url' => 'retour/redirects',
220
            ];
221
        }
222
        $editableSettings = true;
223
        $general = Craft::$app->getConfig()->getGeneral();
224
        if (self::$craft31 && !$general->allowAdminChanges) {
225
            $editableSettings = false;
226
        }
227
        if ($currentUser->can('retour:settings') && $editableSettings) {
228
            $subNavs['settings'] = [
229
                'label' => 'Settings',
230
                'url' => 'retour/settings',
231
            ];
232
        }
233
        $navItem = array_merge($navItem, [
234
            'subnav' => $subNavs,
235
        ]);
236
237
        return $navItem;
238
    }
239
240
    /**
241
     * Clear all the caches!
242
     */
243
    public function clearAllCaches()
244
    {
245
        // Clear all of Retour's caches
246
        self::$plugin->redirects->invalidateCaches();
247
    }
248
249
    // Protected Methods
250
    // =========================================================================
251
252
    /**
253
     * Determine whether our table schema exists or not; this is needed because
254
     * migrations such as the install migration and base_install migration may
255
     * not have been run by the time our init() method has been called
256
     *
257
     * @return bool
258
     */
259
    protected function tableSchemaExists(): bool
260
    {
261
        return (Craft::$app->db->schema->getTableSchema('{{%retour_redirects}}') !== null);
262
    }
263
264
    /**
265
     * Install our event listeners.
266
     */
267
    protected function installEventListeners()
268
    {
269
        // Install our event listeners only if our table schema exists
270
        if ($this->tableSchemaExists()) {
271
            $request = Craft::$app->getRequest();
272
            // Add in our event listeners that are needed for every request
273
            $this->installGlobalEventListeners();
274
            // Install only for non-console site requests
275
            if ($request->getIsSiteRequest() && !$request->getIsConsoleRequest()) {
276
                $this->installSiteEventListeners();
277
            }
278
            // Install only for non-console Control Panel requests
279
            if ($request->getIsCpRequest() && !$request->getIsConsoleRequest()) {
280
                $this->installCpEventListeners();
281
            }
282
        }
283
        // Handler: ClearCaches::EVENT_REGISTER_CACHE_OPTIONS
284
        Event::on(
285
            ClearCaches::class,
286
            ClearCaches::EVENT_REGISTER_CACHE_OPTIONS,
287
            function (RegisterCacheOptionsEvent $event) {
288
                Craft::debug(
289
                    'ClearCaches::EVENT_REGISTER_CACHE_OPTIONS',
290
                    __METHOD__
291
                );
292
                // Register our Control Panel routes
293
                $event->options = array_merge(
294
                    $event->options,
295
                    $this->customAdminCpCacheOptions()
296
                );
297
            }
298
        );
299
        // Handler: EVENT_AFTER_INSTALL_PLUGIN
300
        Event::on(
301
            Plugins::class,
302
            Plugins::EVENT_AFTER_INSTALL_PLUGIN,
303
            function (PluginEvent $event) {
304
                if ($event->plugin === $this) {
305
                    // Invalidate our caches after we've been installed
306
                    $this->clearAllCaches();
307
                    // Send them to our welcome screen
308
                    $request = Craft::$app->getRequest();
309
                    if ($request->isCpRequest) {
310
                        Craft::$app->getResponse()->redirect(UrlHelper::cpUrl(
311
                            'retour/dashboard',
312
                            [
313
                                'showWelcome' => true,
314
                            ]
315
                        ))->send();
316
                    }
317
                }
318
            }
319
        );
320
    }
321
322
    /**
323
     * Install global event listeners for all request types
324
     */
325
    protected function installGlobalEventListeners()
326
    {
327
        // Register the manifest service
328
        $this->set('manifest', [
329
            'class' => ManifestService::class,
330
            'assetClass' => RetourAsset::class,
331
            'devServerManifestPath' => 'http://craft-retour-buildchain:8080/',
332
            'devServerPublicPath' => 'http://craft-retour-buildchain:8080/',
333
        ]);
334
335
        Event::on(
336
            CraftVariable::class,
337
            CraftVariable::EVENT_INIT,
338
            function (Event $event) {
339
                /** @var CraftVariable $variable */
340
                $variable = $event->sender;
341
                $variable->set('retour', [
0 ignored issues
show
The opening parenthesis of a multi-line function call should be the last content on the line.
Loading history...
342
                    'class' => RetourVariable::class,
343
                    'manifestService' => $this->manifest,
344
                ]);
0 ignored issues
show
For multi-line function calls, the closing parenthesis should be on a new line.

If a function call spawns multiple lines, the coding standard suggests to move the closing parenthesis to a new line:

someFunctionCall(
    $firstArgument,
    $secondArgument,
    $thirdArgument
); // Closing parenthesis on a new line.
Loading history...
345
            }
346
        );
347
        // Handler: Elements::EVENT_BEFORE_SAVE_ELEMENT
348
        Event::on(
349
            Elements::class,
350
            Elements::EVENT_BEFORE_SAVE_ELEMENT,
351
            function (ElementEvent $event) {
352
                Craft::debug(
353
                    'Elements::EVENT_BEFORE_SAVE_ELEMENT',
354
                    __METHOD__
355
                );
356
                /** @var Element $element */
357
                $element = $event->element;
358
                if ($element !== null && !$event->isNew && $element->getUrl() !== null && !$element->propagating) {
359
                    $checkElementSlug = true;
360
                    // If we're running Craft 3.2 or later, also check that isn't not a draft or revision
361
                    if (Retour::$craft32 && (
362
                            ElementHelper::isDraftOrRevision($element)
363
                        )) {
364
                        $checkElementSlug = false;
365
                    }
366
                    // Only do this for elements that aren't new, pass $checkElementSlug, and the user
367
                    // has turned on the setting
368
                    if (self::$settings->createUriChangeRedirects && $checkElementSlug) {
369
                        // Make sure this isn't a transitioning temporary draft/revision and that it's
370
                        // not propagating to other sites
371
                        if (strpos($element->uri, '__temp_') === false && !$element->propagating) {
372
                            // Stash the old URLs by element id, and do so only once,
373
                            // in case we are called more than once per request
374
                            if (empty($this->oldElementUris[$element->id])) {
375
                                $this->oldElementUris[$element->id] = $this->getAllElementUris($element);
376
                            }
377
                        }
378
                    }
379
                }
380
            }
381
        );
382
        // Handler: Elements::EVENT_AFTER_SAVE_ELEMENT
383
        Event::on(
384
            Elements::class,
385
            Elements::EVENT_AFTER_SAVE_ELEMENT,
386
            function (ElementEvent $event) {
387
                Craft::debug(
388
                    'Elements::EVENT_AFTER_SAVE_ELEMENT',
389
                    __METHOD__
390
                );
391
                /** @var Element $element */
392
                $element = $event->element;
393
                if ($element !== null && !$event->isNew && $element->getUrl() !== null) {
394
                    $checkElementSlug = true;
395
                    if (Retour::$craft32 && ElementHelper::isDraftOrRevision($element)) {
396
                        $checkElementSlug = false;
397
                    }
398
                    if (self::$settings->createUriChangeRedirects && $checkElementSlug) {
399
                        $this->handleElementUriChange($element);
400
                    }
401
                }
402
            }
403
        );
404
        // Handler: Plugins::EVENT_AFTER_LOAD_PLUGINS
405
        Event::on(
406
            Plugins::class,
407
            Plugins::EVENT_AFTER_LOAD_PLUGINS,
408
            function () {
409
                // Install these only after all other plugins have loaded
410
                $request = Craft::$app->getRequest();
411
                // Only respond to non-console site requests
412
                if ($request->getIsSiteRequest() && !$request->getIsConsoleRequest()) {
413
                    $this->handleSiteRequest();
414
                }
415
                // Respond to Control Panel requests
416
                if ($request->getIsCpRequest() && !$request->getIsConsoleRequest()) {
417
                    $this->handleAdminCpRequest();
418
                }
419
            }
420
        );
421
        if (self::$craft33) {
422
            // Handler: Gql::EVENT_REGISTER_GQL_TYPES
423
            Event::on(
424
                Gql::class,
425
                Gql::EVENT_REGISTER_GQL_TYPES,
426
                function (RegisterGqlTypesEvent $event) {
427
                    Craft::debug(
428
                        'Gql::EVENT_REGISTER_GQL_TYPES',
429
                        __METHOD__
430
                    );
431
                    $event->types[] = RetourInterface::class;
432
                }
433
            );
434
            // Handler: Gql::EVENT_REGISTER_GQL_QUERIES
435
            Event::on(
436
                Gql::class,
437
                Gql::EVENT_REGISTER_GQL_QUERIES,
438
                function (RegisterGqlQueriesEvent $event) {
439
                    Craft::debug(
440
                        'Gql::EVENT_REGISTER_GQL_QUERIES',
441
                        __METHOD__
442
                    );
443
                    $queries = RetourQuery::getQueries();
444
                    foreach ($queries as $key => $value) {
445
                        $event->queries[$key] = $value;
446
                    }
447
                }
448
            );
449
        }
450
        // CraftQL Support
451
        if (class_exists(CraftQL::class)) {
452
            Event::on(
453
                Schema::class,
454
                AlterSchemaFields::EVENT,
455
                [GetCraftQLSchema::class, 'handle']
456
            );
457
        }
458
    }
459
460
    /**
461
     * Install site event listeners for site requests only
462
     */
463
    protected function installSiteEventListeners()
464
    {
465
        // Handler: UrlManager::EVENT_REGISTER_SITE_URL_RULES
466
        Event::on(
467
            UrlManager::class,
468
            UrlManager::EVENT_REGISTER_SITE_URL_RULES,
469
            function (RegisterUrlRulesEvent $event) {
470
                Craft::debug(
471
                    'UrlManager::EVENT_REGISTER_SITE_URL_RULES',
472
                    __METHOD__
473
                );
474
                // Register our Control Panel routes
475
                $event->rules = array_merge(
476
                    $event->rules,
477
                    $this->customFrontendRoutes()
478
                );
479
            }
480
        );
481
    }
482
483
    /**
484
     * Install site event listeners for Control Panel requests only
485
     */
486
    protected function installCpEventListeners()
487
    {
488
        // Handler: Dashboard::EVENT_REGISTER_WIDGET_TYPES
489
        Event::on(
490
            Dashboard::class,
491
            Dashboard::EVENT_REGISTER_WIDGET_TYPES,
492
            function (RegisterComponentTypesEvent $event) {
493
                $event->types[] = RetourWidget::class;
494
            }
495
        );
496
        // Handler: UrlManager::EVENT_REGISTER_CP_URL_RULES
497
        Event::on(
498
            UrlManager::class,
499
            UrlManager::EVENT_REGISTER_CP_URL_RULES,
500
            function (RegisterUrlRulesEvent $event) {
501
                Craft::debug(
502
                    'UrlManager::EVENT_REGISTER_CP_URL_RULES',
503
                    __METHOD__
504
                );
505
                // Register our Control Panel routes
506
                $event->rules = array_merge(
507
                    $event->rules,
508
                    $this->customAdminCpRoutes()
509
                );
510
            }
511
        );
512
        // Handler: UserPermissions::EVENT_REGISTER_PERMISSIONS
513
        Event::on(
514
            UserPermissions::class,
515
            UserPermissions::EVENT_REGISTER_PERMISSIONS,
516
            function (RegisterUserPermissionsEvent $event) {
517
                Craft::debug(
518
                    'UserPermissions::EVENT_REGISTER_PERMISSIONS',
519
                    __METHOD__
520
                );
521
                // Register our custom permissions
522
                $event->permissions[Craft::t('retour', 'Retour')] = $this->customAdminCpPermissions();
523
            }
524
        );
525
    }
526
527
    /**
528
     * Handle site requests.  We do it only after we receive the event
529
     * EVENT_AFTER_LOAD_PLUGINS so that any pending db migrations can be run
530
     * before our event listeners kick in
531
     */
532
    protected function handleSiteRequest()
533
    {
534
        // Handler: ErrorHandler::EVENT_BEFORE_HANDLE_EXCEPTION
535
        Event::on(
536
            ErrorHandler::class,
537
            ErrorHandler::EVENT_BEFORE_HANDLE_EXCEPTION,
538
            function (ExceptionEvent $event) {
539
                Craft::debug(
540
                    'ErrorHandler::EVENT_BEFORE_HANDLE_EXCEPTION',
541
                    __METHOD__
542
                );
543
                $exception = $event->exception;
544
                // If this is a Twig Runtime exception, use the previous one instead
545
                if ($exception instanceof \Twig\Error\RuntimeError &&
546
                    ($previousException = $exception->getPrevious()) !== null) {
547
                    $exception = $previousException;
548
                }
549
                // If this is a 404 error, see if we can handle it
550
                if ($exception instanceof HttpException && $exception->statusCode === 404) {
551
                    self::$currentException = $exception;
552
                    Retour::$plugin->redirects->handle404();
553
                }
554
            }
555
        );
556
    }
557
558
    /**
559
     * Handle Control Panel requests. We do it only after we receive the event
560
     * EVENT_AFTER_LOAD_PLUGINS so that any pending db migrations can be run
561
     * before our event listeners kick in
562
     */
563
    protected function handleAdminCpRequest()
564
    {
565
    }
566
567
    /**
568
     * @inheritdoc
569
     */
570
    protected function createSettingsModel()
571
    {
572
        return new Settings();
573
    }
574
575
    /**
576
     * @param Element $element
577
     */
578
    protected function handleElementUriChange(Element $element)
579
    {
580
        $uris = $this->getAllElementUris($element);
581
        if (!empty($this->oldElementUris[$element->id])) {
582
            $oldElementUris = $this->oldElementUris[$element->id];
583
            foreach ($uris as $siteId => $newUri) {
584
                if (!empty($oldElementUris[$siteId])) {
585
                    $oldUri = $oldElementUris[$siteId];
586
                    Craft::debug(
587
                        Craft::t(
588
                            'retour',
589
                            'Comparing old: {oldUri} to new: {newUri}',
590
                            ['oldUri' => print_r($oldUri, true), 'newUri' => print_r($newUri, true)]
591
                        ),
592
                        __METHOD__
593
                    );
594
                    // Handle the siteId
595
                    $redirectSiteId = null;
596
                    if (Craft::$app->getIsMultiSite()) {
597
                        $redirectSiteId = $siteId;
598
                    }
599
                    // Make sure the URIs are not the same
600
                    if (strcmp($oldUri, $newUri) !== 0) {
601
                        // Handle trailing slash config setting
602
                        if (Craft::$app->config->general->addTrailingSlashesToUrls) {
603
                            $oldUri = rtrim($oldUri, '/') . '/';
604
                            $newUri = rtrim($newUri, '/') . '/';
605
                        }
606
                        // Handle the URL match type
607
                        if (self::$settings->uriChangeRedirectSrcMatch === 'fullurl') {
608
                            try {
609
                                if ($redirectSiteId !== null) {
610
                                    $redirectSiteId = (int)$redirectSiteId;
611
                                }
612
                                $oldUri = UrlHelper::siteUrl($oldUri, null, null, $redirectSiteId);
613
                                $newUri = UrlHelper::siteUrl($newUri, null, null, $redirectSiteId);
614
                            } catch (Exception $e) {
615
                                // That's fine
616
                            }
617
                        }
618
                        $redirectConfig = [
619
                            'id' => 0,
620
                            'redirectMatchType' => 'exactmatch',
621
                            'redirectHttpCode' => 301,
622
                            'redirectSrcMatch' => self::$settings->uriChangeRedirectSrcMatch,
623
                            'redirectSrcUrl' => $oldUri,
624
                            'redirectDestUrl' => $newUri,
625
                            'siteId' => $redirectSiteId,
626
                        ];
627
                        Retour::$plugin->redirects->saveRedirect($redirectConfig);
628
                    }
629
                }
630
            }
631
        }
632
    }
633
634
    /**
635
     * Get the URIs for each site for the element
636
     *
637
     * @param Element $element
638
     *
639
     * @return array
640
     */
641
    protected function getAllElementUris(Element $element): array
642
    {
643
        $uris = [];
644
        if (!self::$craft32 || !ElementHelper::isDraftOrRevision($element)) {
645
            $sites = Craft::$app->getSites()->getAllSites();
646
            foreach ($sites as $site) {
647
                $uri = Craft::$app->getElements()->getElementUriForSite($element->id, $site->id);
648
                if ($uri !== null) {
649
                    $uris[$site->id] = $uri;
650
                }
651
            }
652
        }
653
654
        Craft::debug(
655
            Craft::t(
656
                'retour',
657
                'Getting Element URIs: {uris}',
658
                ['uris' => print_r($uris, true)]
659
            ),
660
            __METHOD__
661
        );
662
663
        return $uris;
664
    }
665
666
    /**
667
     * Return the custom Control Panel routes
668
     *
669
     * @return array
670
     */
671
    protected function customAdminCpRoutes(): array
672
    {
673
        return [
674
            'retour' => 'retour/statistics/dashboard',
675
676
            'retour/redirects' => 'retour/redirects/redirects',
677
            'retour/redirects/<siteHandle:{handle}>' => 'retour/redirects/redirects',
678
679
            'retour/edit-redirect/<redirectId:\d+>' => 'retour/redirects/edit-redirect',
680
681
            'retour/add-redirect' => 'retour/redirects/edit-redirect',
682
            'retour/add-redirect/<siteId:\d+>' => 'retour/redirects/edit-redirect',
683
684
            'retour/dashboard' => 'retour/statistics/dashboard',
685
            'retour/dashboard/<siteHandle:{handle}>' => 'retour/statistics/dashboard',
686
687
            'retour/settings' => 'retour/settings/plugin-settings',
688
        ];
689
    }
690
691
    /**
692
     * Return the custom frontend routes
693
     *
694
     * @return array
695
     */
696
    protected function customFrontendRoutes(): array
697
    {
698
        return [
699
        ];
700
    }
701
702
    /**
703
     * Returns the custom Control Panel cache options.
704
     *
705
     * @return array
706
     */
707
    protected function customAdminCpCacheOptions(): array
708
    {
709
        return [
710
            [
711
                'key' => 'retour-redirect-caches',
712
                'label' => Craft::t('retour', 'Retour redirect caches'),
713
                'action' => [self::$plugin->redirects, 'invalidateCaches'],
714
            ],
715
        ];
716
    }
717
718
    /**
719
     * Returns the custom Control Panel user permissions.
720
     *
721
     * @return array
722
     */
723
    protected function customAdminCpPermissions(): array
724
    {
725
        return [
726
            'retour:dashboard' => [
727
                'label' => Craft::t('retour', 'Dashboard'),
728
            ],
729
            'retour:redirects' => [
730
                'label' => Craft::t('retour', 'Redirects'),
731
            ],
732
            'retour:settings' => [
733
                'label' => Craft::t('retour', 'Settings'),
734
            ],
735
        ];
736
    }
737
}
738