Passed
Push — develop ( 417d14...b031b8 )
by Andrew
07:50
created

Retour::getCpNavItem()   B

Complexity

Conditions 7
Paths 16

Size

Total Lines 34
Code Lines 22

Duplication

Lines 0
Ratio 0 %

Importance

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

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

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

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