Passed
Push — develop ( 63353e...0bb0c7 )
by Andrew
07:54
created

Retour   D

Complexity

Total Complexity 58

Size/Duplication

Total Lines 561
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 58
eloc 238
dl 0
loc 561
rs 4.5599
c 0
b 0
f 0

18 Methods

Rating   Name   Duplication   Size   Complexity  
A installCpEventListeners() 0 37 1
A customFrontendRoutes() 0 10 1
A tableSchemaExists() 0 3 1
A handleSiteRequest() 0 21 5
B handleElementUriChange() 0 32 6
A getAllElementUris() 0 21 3
A customAdminCpCacheOptions() 0 7 1
A clearAllCaches() 0 4 1
C installGlobalEventListeners() 0 77 15
B installEventListeners() 0 49 8
A customAdminCpRoutes() 0 19 1
A createSettingsModel() 0 3 1
A getSettingsResponse() 0 4 1
B getCpNavItem() 0 34 7
A init() 0 26 3
A customAdminCpPermissions() 0 11 1
A installSiteEventListeners() 0 15 1
A handleAdminCpRequest() 0 2 1

How to fix   Complexity   

Complex Class

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

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

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

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\listeners\GetCraftQLSchema;
15
use nystudio107\retour\models\Settings;
16
use nystudio107\retour\services\Redirects;
17
use nystudio107\retour\services\Statistics;
18
use nystudio107\retour\variables\RetourVariable;
19
use nystudio107\retour\widgets\RetourWidget;
20
21
use Craft;
22
use craft\base\Element;
23
use craft\base\Plugin;
24
use craft\events\ElementEvent;
25
use craft\events\ExceptionEvent;
26
use craft\events\PluginEvent;
27
use craft\events\RegisterCacheOptionsEvent;
28
use craft\events\RegisterComponentTypesEvent;
29
use craft\events\RegisterUrlRulesEvent;
30
use craft\events\RegisterUserPermissionsEvent;
31
use craft\helpers\UrlHelper;
32
use craft\services\Elements;
33
use craft\services\Dashboard;
34
use craft\services\Plugins;
35
use craft\services\UserPermissions;
36
use craft\utilities\ClearCaches;
37
use craft\web\ErrorHandler;
38
use craft\web\twig\variables\CraftVariable;
39
use craft\web\UrlManager;
40
41
use yii\base\Event;
42
use yii\web\HttpException;
43
44
use markhuot\CraftQL\Builders\Schema;
0 ignored issues
show
Bug introduced by
The type markhuot\CraftQL\Builders\Schema 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...
45
use markhuot\CraftQL\CraftQL;
0 ignored issues
show
Bug introduced by
The type markhuot\CraftQL\CraftQL 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...
46
use markhuot\CraftQL\Events\AlterSchemaFields;
0 ignored issues
show
Bug introduced by
The type markhuot\CraftQL\Events\AlterSchemaFields 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...
47
48
/** @noinspection MissingPropertyAnnotationsInspection */
49
50
/**
51
 * Class Retour
52
 *
53
 * @author    nystudio107
54
 * @package   Retour
55
 * @since     3.0.0
56
 *
57
 * @property  Redirects  $redirects
58
 * @property  Statistics $statistics
59
 */
60
class Retour extends Plugin
61
{
62
    // Constants
63
    // =========================================================================
64
65
    const DEVMODE_CACHE_DURATION = 30;
66
67
    // Static Properties
68
    // =========================================================================
69
70
    /**
71
     * @var Retour
72
     */
73
    public static $plugin;
74
75
    /**
76
     * @var Settings
77
     */
78
    public static $settings;
79
80
    /**
81
     * @var int
82
     */
83
    public static $cacheDuration;
84
85
    /**
86
     * @var HttpException
87
     */
88
    public static $currentException;
89
90
    /**
91
     * @var bool
92
     */
93
    public static $craft31 = false;
94
95
    // Public Properties
96
    // =========================================================================
97
98
    /**
99
     * @var string
100
     */
101
    public $schemaVersion = '3.0.7';
102
103
104
    /**
105
     * @var array The URIs for the element before it was saved
106
     */
107
    public $oldElementUris = [];
108
109
    // Public Methods
110
    // =========================================================================
111
112
    /**
113
     * @inheritdoc
114
     */
115
    public function init()
116
    {
117
        parent::init();
118
        self::$plugin = $this;
119
        // Initialize properties
120
        self::$settings = $this->getSettings();
121
        self::$craft31 = version_compare(Craft::$app->getVersion(), '3.1', '>=');
122
        $this->name = self::$settings->pluginName;
123
        self::$cacheDuration = Craft::$app->getConfig()->getGeneral()->devMode
124
            ? $this::DEVMODE_CACHE_DURATION
125
            : null;
126
        // Handle any console commands
127
        $request = Craft::$app->getRequest();
128
        if ($request->getIsConsoleRequest()) {
129
            $this->controllerNamespace = 'nystudio107\retour\console\controllers';
130
        }
131
        // Install our event listeners
132
        $this->installEventListeners();
133
        // Log that Retour has been loaded
134
        Craft::info(
135
            Craft::t(
136
                'retour',
137
                '{name} plugin loaded',
138
                ['name' => $this->name]
139
            ),
140
            __METHOD__
141
        );
142
    }
143
144
    /**
145
     * @inheritdoc
146
     */
147
    public function getSettingsResponse()
148
    {
149
        // Just redirect to the plugin settings page
150
        Craft::$app->getResponse()->redirect(UrlHelper::cpUrl('retour/settings'));
151
    }
152
153
    /**
154
     * @inheritdoc
155
     */
156
    public function getCpNavItem()
157
    {
158
        $subNavs = [];
159
        $navItem = parent::getCpNavItem();
160
        $currentUser = Craft::$app->getUser()->getIdentity();
161
        // Only show sub-navs the user has permission to view
162
        if ($currentUser->can('retour:dashboard')) {
0 ignored issues
show
Bug introduced by
The method can() does not exist on yii\web\IdentityInterface. It seems like you code against a sub-type of yii\web\IdentityInterface such as craft\elements\User. ( Ignorable by Annotation )

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

162
        if ($currentUser->/** @scrutinizer ignore-call */ can('retour:dashboard')) {
Loading history...
163
            $subNavs['dashboard'] = [
164
                'label' => 'Dashboard',
165
                'url' => 'retour/dashboard',
166
            ];
167
        }
168
        if ($currentUser->can('retour:redirects')) {
169
            $subNavs['redirects'] = [
170
                'label' => 'Redirects',
171
                'url' => 'retour/redirects',
172
            ];
173
        }
174
        $editableSettings = true;
175
        $general = Craft::$app->getConfig()->getGeneral();
176
        if (self::$craft31 && !$general->allowAdminChanges) {
177
            $editableSettings = false;
178
        }
179
        if ($currentUser->can('retour:settings') && $editableSettings) {
180
            $subNavs['settings'] = [
181
                'label' => 'Settings',
182
                'url' => 'retour/settings',
183
            ];
184
        }
185
        $navItem = array_merge($navItem, [
186
            'subnav' => $subNavs,
187
        ]);
188
189
        return $navItem;
190
    }
191
192
    /**
193
     * Clear all the caches!
194
     */
195
    public function clearAllCaches()
196
    {
197
        // Clear all of Retour's caches
198
        self::$plugin->redirects->invalidateCaches();
199
    }
200
201
    // Protected Methods
202
    // =========================================================================
203
204
    /**
205
     * Determine whether our table schema exists or not; this is needed because
206
     * migrations such as the install migration and base_install migration may
207
     * not have been run by the time our init() method has been called
208
     *
209
     * @return bool
210
     */
211
    protected function tableSchemaExists(): bool
212
    {
213
        return (Craft::$app->db->schema->getTableSchema('{{%retour_redirects}}') !== null);
214
    }
215
216
    /**
217
     * Install our event listeners.
218
     */
219
    protected function installEventListeners()
220
    {
221
        // Install our event listeners only if our table schema exists
222
        if ($this->tableSchemaExists()) {
223
            $request = Craft::$app->getRequest();
224
            // Add in our event listeners that are needed for every request
225
            $this->installGlobalEventListeners();
226
            // Install only for non-console site requests
227
            if ($request->getIsSiteRequest() && !$request->getIsConsoleRequest()) {
228
                $this->installSiteEventListeners();
229
            }
230
            // Install only for non-console Control Panel requests
231
            if ($request->getIsCpRequest() && !$request->getIsConsoleRequest()) {
232
                $this->installCpEventListeners();
233
            }
234
        }
235
        // Handler: ClearCaches::EVENT_REGISTER_CACHE_OPTIONS
236
        Event::on(
237
            ClearCaches::class,
238
            ClearCaches::EVENT_REGISTER_CACHE_OPTIONS,
239
            function (RegisterCacheOptionsEvent $event) {
240
                Craft::debug(
241
                    'ClearCaches::EVENT_REGISTER_CACHE_OPTIONS',
242
                    __METHOD__
243
                );
244
                // Register our Control Panel routes
245
                $event->options = array_merge(
246
                    $event->options,
247
                    $this->customAdminCpCacheOptions()
248
                );
249
            }
250
        );
251
        // Handler: EVENT_AFTER_INSTALL_PLUGIN
252
        Event::on(
253
            Plugins::class,
254
            Plugins::EVENT_AFTER_INSTALL_PLUGIN,
255
            function (PluginEvent $event) {
256
                if ($event->plugin === $this) {
257
                    // Invalidate our caches after we've been installed
258
                    $this->clearAllCaches();
259
                    // Send them to our welcome screen
260
                    $request = Craft::$app->getRequest();
261
                    if ($request->isCpRequest) {
262
                        Craft::$app->getResponse()->redirect(UrlHelper::cpUrl(
263
                            'retour/dashboard',
264
                            [
265
                                'showWelcome' => true,
266
                            ]
267
                        ))->send();
268
                    }
269
                }
270
            }
271
        );
272
    }
273
274
    /**
275
     * Install global event listeners for all request types
276
     */
277
    protected function installGlobalEventListeners()
278
    {
279
        Event::on(
280
            CraftVariable::class,
281
            CraftVariable::EVENT_INIT,
282
            function (Event $event) {
283
                /** @var CraftVariable $variable */
284
                $variable = $event->sender;
285
                $variable->set('retour', RetourVariable::class);
286
            }
287
        );
288
        // Handler: Elements::EVENT_BEFORE_SAVE_ELEMENT
289
        Event::on(
290
            Elements::class,
291
            Elements::EVENT_BEFORE_SAVE_ELEMENT,
292
            function (ElementEvent $event) {
293
                Craft::debug(
294
                    'Elements::EVENT_BEFORE_SAVE_ELEMENT',
295
                    __METHOD__
296
                );
297
                /** @var Element $element */
298
                $element = $event->element;
299
                if (!$event->isNew && self::$settings->createUriChangeRedirects) {
300
                    if ($element !== null && $element->getUrl() !== null) {
301
                        // We want the already saved representation of this element, not the one we are passed
302
                        /** @var Element $oldElement */
303
                        $oldElement = Craft::$app->getElements()->getElementById($element->id);
0 ignored issues
show
Bug introduced by
It seems like $element->id can also be of type null; however, parameter $elementId of craft\services\Elements::getElementById() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

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

303
                        $oldElement = Craft::$app->getElements()->getElementById(/** @scrutinizer ignore-type */ $element->id);
Loading history...
304
                        if ($oldElement !== null) {
305
                            // Stash the old URLs by element id, and do so only once,
306
                            // in case we are called more than once per request
307
                            if (empty($this->oldElementUris[$oldElement->id])) {
308
                                $this->oldElementUris[$oldElement->id] = $this->getAllElementUris($oldElement);
309
                            }
310
                        }
311
                    }
312
                }
313
            }
314
        );
315
        // Handler: Elements::EVENT_AFTER_SAVE_ELEMENT
316
        Event::on(
317
            Elements::class,
318
            Elements::EVENT_AFTER_SAVE_ELEMENT,
319
            function (ElementEvent $event) {
320
                Craft::debug(
321
                    'Elements::EVENT_AFTER_SAVE_ELEMENT',
322
                    __METHOD__
323
                );
324
                /** @var Element $element */
325
                $element = $event->element;
326
                if (!$event->isNew && self::$settings->createUriChangeRedirects && $element->getUrl() !== null) {
327
                    $this->handleElementUriChange($element);
328
                }
329
            }
330
        );
331
        // Handler: Plugins::EVENT_AFTER_LOAD_PLUGINS
332
        Event::on(
333
            Plugins::class,
334
            Plugins::EVENT_AFTER_LOAD_PLUGINS,
335
            function () {
336
                // Install these only after all other plugins have loaded
337
                $request = Craft::$app->getRequest();
338
                // Only respond to non-console site requests
339
                if ($request->getIsSiteRequest() && !$request->getIsConsoleRequest()) {
340
                    $this->handleSiteRequest();
341
                }
342
                // Respond to Control Panel requests
343
                if ($request->getIsCpRequest() && !$request->getIsConsoleRequest()) {
344
                    $this->handleAdminCpRequest();
345
                }
346
            }
347
        );
348
        // CraftQL Support
349
        if (class_exists(CraftQL::class)) {
350
            Event::on(
351
                Schema::class,
352
                AlterSchemaFields::EVENT,
353
                [GetCraftQLSchema::class, 'handle']
354
            );
355
        }
356
    }
357
358
    /**
359
     * Install site event listeners for site requests only
360
     */
361
    protected function installSiteEventListeners()
362
    {
363
        // Handler: UrlManager::EVENT_REGISTER_SITE_URL_RULES
364
        Event::on(
365
            UrlManager::class,
366
            UrlManager::EVENT_REGISTER_SITE_URL_RULES,
367
            function (RegisterUrlRulesEvent $event) {
368
                Craft::debug(
369
                    'UrlManager::EVENT_REGISTER_SITE_URL_RULES',
370
                    __METHOD__
371
                );
372
                // Register our Control Panel routes
373
                $event->rules = array_merge(
374
                    $event->rules,
375
                    $this->customFrontendRoutes()
376
                );
377
            }
378
        );
379
    }
380
381
    /**
382
     * Install site event listeners for Control Panel requests only
383
     */
384
    protected function installCpEventListeners()
385
    {
386
        // Handler: Dashboard::EVENT_REGISTER_WIDGET_TYPES
387
        Event::on(
388
            Dashboard::class,
389
            Dashboard::EVENT_REGISTER_WIDGET_TYPES,
390
            function (RegisterComponentTypesEvent $event) {
391
                $event->types[] = RetourWidget::class;
392
            }
393
        );
394
        // Handler: UrlManager::EVENT_REGISTER_CP_URL_RULES
395
        Event::on(
396
            UrlManager::class,
397
            UrlManager::EVENT_REGISTER_CP_URL_RULES,
398
            function (RegisterUrlRulesEvent $event) {
399
                Craft::debug(
400
                    'UrlManager::EVENT_REGISTER_CP_URL_RULES',
401
                    __METHOD__
402
                );
403
                // Register our Control Panel routes
404
                $event->rules = array_merge(
405
                    $event->rules,
406
                    $this->customAdminCpRoutes()
407
                );
408
            }
409
        );
410
        // Handler: UserPermissions::EVENT_REGISTER_PERMISSIONS
411
        Event::on(
412
            UserPermissions::class,
413
            UserPermissions::EVENT_REGISTER_PERMISSIONS,
414
            function (RegisterUserPermissionsEvent $event) {
415
                Craft::debug(
416
                    'UserPermissions::EVENT_REGISTER_PERMISSIONS',
417
                    __METHOD__
418
                );
419
                // Register our custom permissions
420
                $event->permissions[Craft::t('retour', 'Retour')] = $this->customAdminCpPermissions();
421
            }
422
        );
423
    }
424
425
    /**
426
     * Handle site requests.  We do it only after we receive the event
427
     * EVENT_AFTER_LOAD_PLUGINS so that any pending db migrations can be run
428
     * before our event listeners kick in
429
     */
430
    protected function handleSiteRequest()
431
    {
432
        // Handler: ErrorHandler::EVENT_BEFORE_HANDLE_EXCEPTION
433
        Event::on(
434
            ErrorHandler::class,
435
            ErrorHandler::EVENT_BEFORE_HANDLE_EXCEPTION,
436
            function (ExceptionEvent $event) {
437
                Craft::debug(
438
                    'ErrorHandler::EVENT_BEFORE_HANDLE_EXCEPTION',
439
                    __METHOD__
440
                );
441
                $exception = $event->exception;
442
                // If this is a Twig Runtime exception, use the previous one instead
443
                if ($exception instanceof \Twig\Error\RuntimeError &&
444
                    ($previousException = $exception->getPrevious()) !== null) {
445
                    $exception = $previousException;
446
                }
447
                // If this is a 404 error, see if we can handle it
448
                if ($exception instanceof HttpException && $exception->statusCode === 404) {
449
                    self::$currentException = $exception;
450
                    Retour::$plugin->redirects->handle404();
451
                }
452
            }
453
        );
454
    }
455
456
    /**
457
     * Handle Control Panel requests. We do it only after we receive the event
458
     * EVENT_AFTER_LOAD_PLUGINS so that any pending db migrations can be run
459
     * before our event listeners kick in
460
     */
461
    protected function handleAdminCpRequest()
462
    {
463
    }
464
465
    /**
466
     * @inheritdoc
467
     */
468
    protected function createSettingsModel()
469
    {
470
        return new Settings();
471
    }
472
473
    /**
474
     * @param Element $element
475
     */
476
    protected function handleElementUriChange(Element $element)
477
    {
478
        $uris = $this->getAllElementUris($element);
479
        if (!empty($this->oldElementUris[$element->id])) {
480
            $oldElementUris = $this->oldElementUris[$element->id];
481
            foreach ($uris as $siteId => $newUri) {
482
                if (!empty($oldElementUris[$siteId])) {
483
                    $oldUri = $oldElementUris[$siteId];
484
                    Craft::debug(
485
                        Craft::t(
486
                            'retour',
487
                            'Comparing old: {oldUri} to new: {newUri}',
488
                            ['oldUri' => print_r($oldUri, true), 'newUri' => print_r($newUri, true)]
489
                        ),
490
                        __METHOD__
491
                    );
492
                    // Handle the siteId
493
                    $redirectSiteId = null;
494
                    if (Craft::$app->getIsMultiSite()) {
495
                        $redirectSiteId = $siteId;
496
                    }
497
                    // Make sure the URIs are not the same
498
                    if (strcmp($oldUri, $newUri) !== 0) {
499
                        $redirectConfig = [
500
                            'id' => 0,
501
                            'redirectMatchType' => 'exactmatch',
502
                            'redirectHttpCode' => 301,
503
                            'redirectSrcUrl' => $oldUri,
504
                            'redirectDestUrl' => $newUri,
505
                            'siteId' => $redirectSiteId,
506
                        ];
507
                        Retour::$plugin->redirects->saveRedirect($redirectConfig);
508
                    }
509
                }
510
            }
511
        }
512
    }
513
514
    /**
515
     * Get the URIs for each site for the element
516
     *
517
     * @param Element $element
518
     *
519
     * @return array
520
     */
521
    protected function getAllElementUris(Element $element): array
522
    {
523
        $uris = [];
524
        $sites = Craft::$app->getSites()->getAllSites();
525
        foreach ($sites as $site) {
526
            $uri = Craft::$app->getElements()->getElementUriForSite($element->id, $site->id);
0 ignored issues
show
Bug introduced by
It seems like $element->id can also be of type null; however, parameter $elementId of craft\services\Elements::getElementUriForSite() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

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

526
            $uri = Craft::$app->getElements()->getElementUriForSite(/** @scrutinizer ignore-type */ $element->id, $site->id);
Loading history...
527
            if ($uri !== null) {
528
                $uris[$site->id] = $uri;
529
            }
530
        }
531
532
        Craft::debug(
533
            Craft::t(
534
                'retour',
535
                'Getting Element URIs: {uris}',
536
                ['uris' => print_r($uris, true)]
537
            ),
538
            __METHOD__
539
        );
540
541
        return $uris;
542
    }
543
544
    /**
545
     * Return the custom Control Panel routes
546
     *
547
     * @return array
548
     */
549
    protected function customAdminCpRoutes(): array
550
    {
551
        return [
552
            'retour' => 'retour/statistics/dashboard',
553
554
            'retour/redirects' => 'retour/redirects/redirects',
555
            'retour/redirects/<siteHandle:{handle}>' => 'retour/redirects/redirects',
556
557
            'retour/edit-redirect/<redirectId:\d+>' => 'retour/redirects/edit-redirect',
558
559
            'retour/add-redirect' => 'retour/redirects/edit-redirect',
560
            'retour/add-redirect/<siteId:\d+>' => 'retour/redirects/edit-redirect',
561
562
            'retour/delete-redirect/<redirectId:\d+>' => 'retour/redirects/delete-redirect',
563
564
            'retour/dashboard' => 'retour/statistics/dashboard',
565
            'retour/dashboard/<siteHandle:{handle}>' => 'retour/statistics/dashboard',
566
567
            'retour/settings' => 'retour/settings/plugin-settings',
568
        ];
569
    }
570
571
    /**
572
     * Return the custom frontend routes
573
     *
574
     * @return array
575
     */
576
    protected function customFrontendRoutes(): array
577
    {
578
        return [
579
            // Tables
580
            '/retour/tables/dashboard' => 'retour/tables/dashboard',
581
            '/retour/tables/redirects' => 'retour/tables/redirects',
582
            // Charts
583
            '/retour/charts/dashboard/<range:{handle}>' => 'retour/charts/dashboard',
584
            '/retour/charts/dashboard/<range:{handle}>/<siteId:\d+>' => 'retour/charts/dashboard',
585
            '/retour/charts/widget/<days>' => 'retour/charts/widget',
586
        ];
587
    }
588
589
    /**
590
     * Returns the custom Control Panel cache options.
591
     *
592
     * @return array
593
     */
594
    protected function customAdminCpCacheOptions(): array
595
    {
596
        return [
597
            [
598
                'key' => 'retour-redirect-caches',
599
                'label' => Craft::t('retour', 'Retour redirect caches'),
600
                'action' => [self::$plugin->redirects, 'invalidateCaches'],
601
            ],
602
        ];
603
    }
604
605
    /**
606
     * Returns the custom Control Panel user permissions.
607
     *
608
     * @return array
609
     */
610
    protected function customAdminCpPermissions(): array
611
    {
612
        return [
613
            'retour:dashboard' => [
614
                'label' => Craft::t('retour', 'Dashboard'),
615
            ],
616
            'retour:redirects' => [
617
                'label' => Craft::t('retour', 'Redirects'),
618
            ],
619
            'retour:settings' => [
620
                'label' => Craft::t('retour', 'Settings'),
621
            ],
622
        ];
623
    }
624
}
625