Issues (1834)

src/Retour.php (4 issues)

1
<?php
2
/**
3
 * Retour plugin for Craft CMS
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 Craft;
15
use craft\base\Element;
16
use craft\base\Plugin;
17
use craft\events\ElementEvent;
18
use craft\events\ExceptionEvent;
19
use craft\events\PluginEvent;
20
use craft\events\RegisterCacheOptionsEvent;
21
use craft\events\RegisterComponentTypesEvent;
22
use craft\events\RegisterGqlQueriesEvent;
23
use craft\events\RegisterGqlSchemaComponentsEvent;
24
use craft\events\RegisterGqlTypesEvent;
25
use craft\events\RegisterUrlRulesEvent;
26
use craft\events\RegisterUserPermissionsEvent;
27
use craft\helpers\ElementHelper;
28
use craft\helpers\UrlHelper;
29
use craft\services\Dashboard;
30
use craft\services\Elements;
31
use craft\services\Fields;
32
use craft\services\Gql;
33
use craft\services\Plugins;
34
use craft\services\UserPermissions;
35
use craft\utilities\ClearCaches;
36
use craft\web\ErrorHandler;
37
use craft\web\twig\variables\CraftVariable;
38
use craft\web\UrlManager;
39
use craft\web\User;
40
use markhuot\CraftQL\Builders\Schema;
41
use markhuot\CraftQL\CraftQL;
42
use markhuot\CraftQL\Events\AlterSchemaFields;
43
use nystudio107\retour\fields\ShortLink as ShortLinkField;
44
use nystudio107\retour\gql\interfaces\RetourInterface;
45
use nystudio107\retour\gql\queries\RetourQuery;
46
use nystudio107\retour\listeners\GetCraftQLSchema;
47
use nystudio107\retour\models\Settings;
48
use nystudio107\retour\services\ServicesTrait;
49
use nystudio107\retour\variables\RetourVariable;
50
use nystudio107\retour\widgets\RetourWidget;
51
use Twig\Error\RuntimeError;
52
use yii\base\Event;
53
use yii\web\HttpException;
54
55
/** @noinspection MissingPropertyAnnotationsInspection */
56
57
/**
58
 * Class Retour
59
 *
60
 * @author    nystudio107
61
 * @package   Retour
62
 * @since     3.0.0
63
 * @method Settings getSettings()
64
 */
65
class Retour extends Plugin
66
{
67
    // Traits
68
    // =========================================================================
69
70
    use ServicesTrait;
71
72
    // Constants
73
    // =========================================================================
74
75
    const DEVMODE_CACHE_DURATION = 30;
76
77
    // Static Properties
78
    // =========================================================================
79
80
    /**
81
     * @var Retour
82
     */
83
    public static $plugin;
84
85
    /**
86
     * @var Settings
87
     */
88
    public static $settings;
89
90
    /**
91
     * @var int
92
     */
93
    public static $cacheDuration;
94
95
    /**
96
     * @var HttpException
97
     */
98
    public static $currentException;
99
100
    /**
101
     * @var bool
102
     */
103
    public static $craft31 = false;
104
105
    /**
106
     * @var bool
107
     */
108
    public static $craft32 = false;
109
110
    /**
111
     * @var bool
112
     */
113
    public static $craft33 = false;
114
115
    /**
116
     * @var bool
117
     */
118
    public static $craft35 = false;
119
120
    // Public Properties
121
    // =========================================================================
122
123
    /**
124
     * @var string
125
     */
126
    public $schemaVersion = '3.0.12';
127
128
    /**
129
     * @var bool
130
     */
131
    public $hasCpSection = true;
132
133
    /**
134
     * @var bool
135
     */
136
    public $hasCpSettings = true;
137
138
    // Public Methods
139
    // =========================================================================
140
141
    /**
142
     * @inheritdoc
143
     */
144
    public function init()
145
    {
146
        parent::init();
147
        self::$plugin = $this;
148
        // Initialize properties
149
        self::$settings = $this->getSettings();
150
        self::$craft31 = version_compare(Craft::$app->getVersion(), '3.1', '>=');
0 ignored issues
show
Documentation Bug introduced by
It seems like version_compare(Craft::a...Version(), '3.1', '>=') can also be of type integer. However, the property $craft31 is declared as type boolean. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
151
        self::$craft32 = version_compare(Craft::$app->getVersion(), '3.2', '>=');
0 ignored issues
show
Documentation Bug introduced by
It seems like version_compare(Craft::a...Version(), '3.2', '>=') can also be of type integer. However, the property $craft32 is declared as type boolean. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
152
        self::$craft33 = version_compare(Craft::$app->getVersion(), '3.3', '>=');
0 ignored issues
show
Documentation Bug introduced by
It seems like version_compare(Craft::a...Version(), '3.3', '>=') can also be of type integer. However, the property $craft33 is declared as type boolean. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
153
        self::$craft35 = version_compare(Craft::$app->getVersion(), '3.5', '>=');
0 ignored issues
show
Documentation Bug introduced by
It seems like version_compare(Craft::a...Version(), '3.5', '>=') can also be of type integer. However, the property $craft35 is declared as type boolean. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
154
        $this->name = self::$settings->pluginName;
155
        self::$cacheDuration = Craft::$app->getConfig()->getGeneral()->devMode
156
            ? $this::DEVMODE_CACHE_DURATION
157
            : null;
158
        // Handle any console commands
159
        $request = Craft::$app->getRequest();
160
        if ($request->getIsConsoleRequest()) {
161
            $this->controllerNamespace = 'nystudio107\retour\console\controllers';
162
        }
163
        // Install our event listeners
164
        $this->installEventListeners();
165
        // Log that Retour has been loaded
166
        Craft::info(
167
            Craft::t(
168
                'retour',
169
                '{name} plugin loaded',
170
                ['name' => $this->name]
171
            ),
172
            __METHOD__
173
        );
174
    }
175
176
    /**
177
     * @inheritdoc
178
     */
179
    public function getSettingsResponse()
180
    {
181
        // Just redirect to the plugin settings page
182
        Craft::$app->getResponse()->redirect(UrlHelper::cpUrl('retour/settings'));
183
    }
184
185
    /**
186
     * @inheritdoc
187
     */
188
    public function getCpNavItem()
189
    {
190
        $subNavs = [];
191
        $navItem = parent::getCpNavItem();
192
        /** @var User $user */
193
        $user = Craft::$app->getUser();
194
        if ($currentUser = $user->getIdentity()) {
195
            // Only show sub-navs the user has permission to view
196
            if ($currentUser->can('retour:dashboard')) {
197
                $subNavs['dashboard'] = [
198
                    'label' => 'Dashboard',
199
                    'url' => 'retour/dashboard',
200
                ];
201
            }
202
            if ($currentUser->can('retour:redirects')) {
203
                $subNavs['redirects'] = [
204
                    'label' => 'Redirects',
205
                    'url' => 'retour/redirects',
206
                ];
207
            }
208
            if ($currentUser->can('retour:shortlinks')) {
209
                $subNavs['shortlinks'] = [
210
                    'label' => 'Short Links',
211
                    'url' => 'retour/shortlinks',
212
                ];
213
            }
214
            $editableSettings = true;
215
            $general = Craft::$app->getConfig()->getGeneral();
216
            if (self::$craft31 && !$general->allowAdminChanges) {
217
                $editableSettings = false;
218
            }
219
            if ($currentUser->can('retour:settings') && $editableSettings) {
220
                $subNavs['settings'] = [
221
                    'label' => 'Settings',
222
                    'url' => 'retour/settings',
223
                ];
224
            }
225
        }
226
        // Retour doesn't really have an index page, so if the user can't access any sub nav items, we probably shouldn't show the main sub nav item either
227
        if (empty($subNavs)) {
228
            return null;
229
        }
230
        // A single sub nav item is redundant
231
        if (count($subNavs) === 1) {
232
            $subNavs = [];
233
        }
234
        $navItem = array_merge($navItem, [
235
            'subnav' => $subNavs,
236
        ]);
237
238
        return $navItem;
239
    }
240
241
    /**
242
     * Clear all the caches!
243
     */
244
    public function clearAllCaches()
245
    {
246
        // Clear all of Retour's caches
247
        self::$plugin->redirects->invalidateCaches();
248
    }
249
250
    // Protected Methods
251
    // =========================================================================
252
253
    /**
254
     * Determine whether our table schema exists or not; this is needed because
255
     * migrations such as the install migration and base_install migration may
256
     * not have been run by the time our init() method has been called
257
     *
258
     * @return bool
259
     */
260
    protected function tableSchemaExists(): bool
261
    {
262
        return (Craft::$app->db->schema->getTableSchema('{{%retour_redirects}}') !== null);
263
    }
264
265
    /**
266
     * Install our event listeners.
267
     */
268
    protected function installEventListeners()
269
    {
270
        // Install our event listeners only if our table schema exists
271
        if ($this->tableSchemaExists()) {
272
            $request = Craft::$app->getRequest();
273
            // Add in our event listeners that are needed for every request
274
            $this->installGlobalEventListeners();
275
            // Install only for non-console site requests
276
            if ($request->getIsSiteRequest() && !$request->getIsConsoleRequest()) {
277
                $this->installSiteEventListeners();
278
            }
279
            // Install only for non-console Control Panel requests
280
            if ($request->getIsCpRequest() && !$request->getIsConsoleRequest()) {
281
                $this->installCpEventListeners();
282
            }
283
        }
284
        // Handler: ClearCaches::EVENT_REGISTER_CACHE_OPTIONS
285
        Event::on(
286
            ClearCaches::class,
287
            ClearCaches::EVENT_REGISTER_CACHE_OPTIONS,
288
            function(RegisterCacheOptionsEvent $event) {
289
                Craft::debug(
290
                    'ClearCaches::EVENT_REGISTER_CACHE_OPTIONS',
291
                    __METHOD__
292
                );
293
                // Register our Control Panel routes
294
                $event->options = array_merge(
295
                    $event->options,
296
                    $this->customAdminCpCacheOptions()
297
                );
298
            }
299
        );
300
        // Handler: EVENT_AFTER_INSTALL_PLUGIN
301
        Event::on(
302
            Plugins::class,
303
            Plugins::EVENT_AFTER_INSTALL_PLUGIN,
304
            function(PluginEvent $event) {
305
                if ($event->plugin === $this) {
306
                    // Invalidate our caches after we've been installed
307
                    $this->clearAllCaches();
308
                    // Send them to our welcome screen
309
                    $request = Craft::$app->getRequest();
310
                    if ($request->isCpRequest) {
311
                        Craft::$app->getResponse()->redirect(UrlHelper::cpUrl(
312
                            'retour/dashboard',
313
                            [
314
                                'showWelcome' => true,
315
                            ]
316
                        ))->send();
317
                    }
318
                }
319
            }
320
        );
321
    }
322
323
    /**
324
     * Install global event listeners for all request types
325
     */
326
    protected function installGlobalEventListeners()
327
    {
328
        Event::on(
329
            CraftVariable::class,
330
            CraftVariable::EVENT_INIT,
331
            function(Event $event) {
332
                /** @var CraftVariable $variable */
333
                $variable = $event->sender;
334
                $variable->set('retour', [
335
                    'class' => RetourVariable::class,
336
                    'viteService' => $this->vite,
337
                ]);
338
            }
339
        );
340
341
        $prepareRedirectOnElementChange = function(ElementEvent $event) {
342
            /** @var Element $element */
343
            $element = $event->element;
344
            if ($element !== null && !$element->propagating && !$event->isNew && $element->getUrl() !== null) {
345
                $checkElementSlug = true;
346
                // Make sure the element is enabled
347
                if (!$element->enabled || !$element->getEnabledForSite()) {
348
                    $checkElementSlug = false;
349
                }
350
                // If we're running Craft 3.2 or later, also check that isn't not a draft or revision
351
                if (Retour::$craft32 && (
352
                    ElementHelper::isDraftOrRevision($element)
353
                    )) {
354
                    $checkElementSlug = false;
355
                }
356
                // Only do this for elements that aren't new, pass $checkElementSlug, and the user
357
                // has turned on the setting
358
                if (self::$settings->createUriChangeRedirects && $checkElementSlug) {
359
                    // Make sure this isn't a transitioning temporary draft/revision and that it's
360
                    // not propagating to other sites
361
                    if ($element->uri && !str_contains($element->uri, '__temp_')) {
362
                        Retour::$plugin->events->stashElementUris($element);
363
                    }
364
                }
365
            }
366
        };
367
368
        $insertRedirectOnElementChange = function(ElementEvent $event) {
369
            /** @var Element $element */
370
            $element = $event->element;
371
            if ($element !== null && !$event->isNew && $element->getUrl() !== null) {
372
                $checkElementSlug = true;
373
                // Make sure the element is enabled
374
                if (!$element->enabled || !$element->getEnabledForSite()) {
375
                    $checkElementSlug = false;
376
                }
377
                if (Retour::$craft32 && ElementHelper::isDraftOrRevision($element)) {
378
                    $checkElementSlug = false;
379
                }
380
                if (self::$settings->createUriChangeRedirects && $checkElementSlug) {
381
                    Retour::$plugin->events->handleElementUriChange($element);
382
                }
383
            }
384
        };
385
386
        // Handler: Elements::EVENT_BEFORE_SAVE_ELEMENT
387
        Event::on(
388
            Elements::class,
389
            Elements::EVENT_BEFORE_SAVE_ELEMENT,
390
            function(ElementEvent $event) use ($prepareRedirectOnElementChange) {
391
                Craft::debug(
392
                    'Elements::EVENT_BEFORE_SAVE_ELEMENT',
393
                    __METHOD__
394
                );
395
                $prepareRedirectOnElementChange($event);
396
            }
397
        );
398
        // Handler: Elements::EVENT_AFTER_SAVE_ELEMENT
399
        Event::on(
400
            Elements::class,
401
            Elements::EVENT_AFTER_SAVE_ELEMENT,
402
            function(ElementEvent $event) use ($insertRedirectOnElementChange) {
403
                Craft::debug(
404
                    'Elements::EVENT_AFTER_SAVE_ELEMENT',
405
                    __METHOD__
406
                );
407
                $insertRedirectOnElementChange($event);
408
            }
409
        );
410
        // Handler: Elements::EVENT_BEFORE_UPDATE_SLUG_AND_URI
411
        Event::on(
412
            Elements::class,
413
            Elements::EVENT_BEFORE_UPDATE_SLUG_AND_URI,
414
            function(ElementEvent $event) use ($prepareRedirectOnElementChange) {
415
                Craft::debug(
416
                    'Elements::EVENT_BEFORE_UPDATE_SLUG_AND_URI',
417
                    __METHOD__
418
                );
419
                $prepareRedirectOnElementChange($event);
420
            }
421
        );
422
        // Handler: Elements::EVENT_AFTER_UPDATE_SLUG_AND_URI
423
        Event::on(
424
            Elements::class,
425
            Elements::EVENT_AFTER_UPDATE_SLUG_AND_URI,
426
            function(ElementEvent $event) use ($insertRedirectOnElementChange) {
427
                Craft::debug(
428
                    'Elements::EVENT_AFTER_UPDATE_SLUG_AND_URI',
429
                    __METHOD__
430
                );
431
                $insertRedirectOnElementChange($event);
432
            }
433
        );
434
435
        // Handler: Plugins::EVENT_AFTER_LOAD_PLUGINS
436
        Event::on(
437
            Plugins::class,
438
            Plugins::EVENT_AFTER_LOAD_PLUGINS,
439
            function() {
440
                // Install these only after all other plugins have loaded
441
                $request = Craft::$app->getRequest();
442
                // Only respond to non-console site requests
443
                if ($request->getIsSiteRequest() && !$request->getIsConsoleRequest()) {
444
                    $this->handleSiteRequest();
445
                }
446
                // Respond to Control Panel requests
447
                if ($request->getIsCpRequest() && !$request->getIsConsoleRequest()) {
448
                    $this->handleAdminCpRequest();
449
                }
450
            }
451
        );
452
        // Handler: Fields::EVENT_REGISTER_FIELD_TYPES
453
        Event::on(
454
            Fields::class,
455
            Fields::EVENT_REGISTER_FIELD_TYPES,
456
            function(RegisterComponentTypesEvent $event) {
457
                $event->types[] = ShortLinkField::class;
458
            }
459
        );
460
        if (self::$craft33) {
461
            // Handler: Gql::EVENT_REGISTER_GQL_TYPES
462
            Event::on(
463
                Gql::class,
464
                Gql::EVENT_REGISTER_GQL_TYPES,
465
                function(RegisterGqlTypesEvent $event) {
466
                    Craft::debug(
467
                        'Gql::EVENT_REGISTER_GQL_TYPES',
468
                        __METHOD__
469
                    );
470
                    $event->types[] = RetourInterface::class;
471
                }
472
            );
473
            // Handler: Gql::EVENT_REGISTER_GQL_QUERIES
474
            Event::on(
475
                Gql::class,
476
                Gql::EVENT_REGISTER_GQL_QUERIES,
477
                function(RegisterGqlQueriesEvent $event) {
478
                    Craft::debug(
479
                        'Gql::EVENT_REGISTER_GQL_QUERIES',
480
                        __METHOD__
481
                    );
482
                    $queries = RetourQuery::getQueries();
483
                    foreach ($queries as $key => $value) {
484
                        $event->queries[$key] = $value;
485
                    }
486
                }
487
            );
488
            if (self::$craft35) {
489
                // Handler: Gql::EVENT_REGISTER_SCHEMA_COMPONENTS
490
                Event::on(
491
                    Gql::class,
492
                    Gql::EVENT_REGISTER_GQL_SCHEMA_COMPONENTS,
493
                    function(RegisterGqlSchemaComponentsEvent $event) {
494
                        Craft::debug(
495
                            'Gql::EVENT_REGISTER_GQL_SCHEMA_COMPONENTS',
496
                            __METHOD__
497
                        );
498
                        $label = Craft::t('retour', 'Retour');
499
                        $event->queries[$label]['retour.all:read'] = ['label' => Craft::t('retour', 'Query Retour data')];
500
                    }
501
                );
502
            }
503
        }
504
        // CraftQL Support
505
        if (class_exists(CraftQL::class)) {
506
            Event::on(
507
                Schema::class,
508
                AlterSchemaFields::EVENT,
509
                [GetCraftQLSchema::class, 'handle']
510
            );
511
        }
512
    }
513
514
    /**
515
     * Install site event listeners for site requests only
516
     */
517
    protected function installSiteEventListeners()
518
    {
519
        // Handler: UrlManager::EVENT_REGISTER_SITE_URL_RULES
520
        Event::on(
521
            UrlManager::class,
522
            UrlManager::EVENT_REGISTER_SITE_URL_RULES,
523
            function(RegisterUrlRulesEvent $event) {
524
                Craft::debug(
525
                    'UrlManager::EVENT_REGISTER_SITE_URL_RULES',
526
                    __METHOD__
527
                );
528
                // Register our Control Panel routes
529
                $event->rules = array_merge(
530
                    $event->rules,
531
                    $this->customFrontendRoutes()
532
                );
533
            }
534
        );
535
    }
536
537
    /**
538
     * Install site event listeners for Control Panel requests only
539
     */
540
    protected function installCpEventListeners()
541
    {
542
        // Handler: Dashboard::EVENT_REGISTER_WIDGET_TYPES
543
        Event::on(
544
            Dashboard::class,
545
            Dashboard::EVENT_REGISTER_WIDGET_TYPES,
546
            function(RegisterComponentTypesEvent $event) {
547
                /** @var User $user */
548
                $user = Craft::$app->getUser();
549
                if ($currentUser = $user->getIdentity()) {
550
                    if ($currentUser->can('accessPlugin-retour')) {
551
                        $event->types[] = RetourWidget::class;
552
                    }
553
                }
554
            }
555
        );
556
        // Handler: UrlManager::EVENT_REGISTER_CP_URL_RULES
557
        Event::on(
558
            UrlManager::class,
559
            UrlManager::EVENT_REGISTER_CP_URL_RULES,
560
            function(RegisterUrlRulesEvent $event) {
561
                Craft::debug(
562
                    'UrlManager::EVENT_REGISTER_CP_URL_RULES',
563
                    __METHOD__
564
                );
565
                // Register our Control Panel routes
566
                $event->rules = array_merge(
567
                    $event->rules,
568
                    $this->customAdminCpRoutes()
569
                );
570
            }
571
        );
572
        // Handler: UserPermissions::EVENT_REGISTER_PERMISSIONS
573
        Event::on(
574
            UserPermissions::class,
575
            UserPermissions::EVENT_REGISTER_PERMISSIONS,
576
            function(RegisterUserPermissionsEvent $event) {
577
                Craft::debug(
578
                    'UserPermissions::EVENT_REGISTER_PERMISSIONS',
579
                    __METHOD__
580
                );
581
                // Register our custom permissions
582
                $event->permissions[Craft::t('retour', 'Retour')] = $this->customAdminCpPermissions();
583
            }
584
        );
585
    }
586
587
    /**
588
     * Handle site requests.  We do it only after we receive the event
589
     * EVENT_AFTER_LOAD_PLUGINS so that any pending db migrations can be run
590
     * before our event listeners kick in
591
     */
592
    protected function handleSiteRequest()
593
    {
594
        // Handler: ErrorHandler::EVENT_BEFORE_HANDLE_EXCEPTION
595
        Event::on(
596
            ErrorHandler::class,
597
            ErrorHandler::EVENT_BEFORE_HANDLE_EXCEPTION,
598
            function(ExceptionEvent $event) {
599
                Craft::debug(
600
                    'ErrorHandler::EVENT_BEFORE_HANDLE_EXCEPTION',
601
                    __METHOD__
602
                );
603
                $exception = $event->exception;
604
                // If this is a Twig Runtime exception, use the previous one instead
605
                if ($exception instanceof RuntimeError &&
606
                    ($previousException = $exception->getPrevious()) !== null) {
607
                    $exception = $previousException;
608
                }
609
                // If this is a 404 error, see if we can handle it
610
                if ($exception instanceof HttpException && $exception->statusCode === 404) {
611
                    self::$currentException = $exception;
612
                    Retour::$plugin->redirects->handle404();
613
                }
614
            }
615
        );
616
    }
617
618
    /**
619
     * Handle Control Panel requests. We do it only after we receive the event
620
     * EVENT_AFTER_LOAD_PLUGINS so that any pending db migrations can be run
621
     * before our event listeners kick in
622
     */
623
    protected function handleAdminCpRequest()
624
    {
625
    }
626
627
    /**
628
     * @inheritdoc
629
     */
630
    protected function createSettingsModel()
631
    {
632
        return new Settings();
633
    }
634
635
    /**
636
     * Return the custom Control Panel routes
637
     *
638
     * @return array
639
     */
640
    protected function customAdminCpRoutes(): array
641
    {
642
        return [
643
            'retour' => '',
644
645
            'retour/redirects' => 'retour/redirects/redirects',
646
            'retour/redirects/<siteHandle:{handle}>' => 'retour/redirects/redirects',
647
648
            'retour/edit-redirect/<redirectId:\d+>' => 'retour/redirects/edit-redirect',
649
650
            'retour/add-redirect' => 'retour/redirects/edit-redirect',
651
            'retour/add-redirect/<siteId:\d+>' => 'retour/redirects/edit-redirect',
652
653
            'retour/dashboard' => 'retour/statistics/dashboard',
654
            'retour/dashboard/<siteHandle:{handle}>' => 'retour/statistics/dashboard',
655
656
            'retour/shortlinks' => 'retour/redirects/shortlinks',
657
            'retour/shortlinks/<siteHandle:{handle}>' => 'retour/redirects/shortlinks',
658
659
            'retour/settings' => 'retour/settings/plugin-settings',
660
        ];
661
    }
662
663
    /**
664
     * Return the custom frontend routes
665
     *
666
     * @return array
667
     */
668
    protected function customFrontendRoutes(): array
669
    {
670
        return [
671
        ];
672
    }
673
674
    /**
675
     * Returns the custom Control Panel cache options.
676
     *
677
     * @return array
678
     */
679
    protected function customAdminCpCacheOptions(): array
680
    {
681
        return [
682
            [
683
                'key' => 'retour-redirect-caches',
684
                'label' => Craft::t('retour', 'Retour redirect caches'),
685
                'action' => [self::$plugin->redirects, 'invalidateCaches'],
686
            ],
687
        ];
688
    }
689
690
    /**
691
     * Returns the custom Control Panel user permissions.
692
     *
693
     * @return array
694
     */
695
    protected function customAdminCpPermissions(): array
696
    {
697
        return [
698
            'retour:dashboard' => [
699
                'label' => Craft::t('retour', 'Dashboard'),
700
            ],
701
            'retour:redirects' => [
702
                'label' => Craft::t('retour', 'Redirects'),
703
            ],
704
            'retour:shortlinks' => [
705
                'label' => Craft::t('retour', 'Short Links'),
706
            ],
707
            'retour:settings' => [
708
                'label' => Craft::t('retour', 'Settings'),
709
            ],
710
        ];
711
    }
712
}
713