Passed
Push — develop ( e41f77...af481c )
by Andrew
08:50 queued 03:36
created

Retour::handleElementUriChange()   B

Complexity

Conditions 6
Paths 7

Size

Total Lines 32
Code Lines 24

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 24
dl 0
loc 32
rs 8.9137
c 0
b 0
f 0
cc 6
nc 7
nop 1
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
    // Public Properties
86
    // =========================================================================
87
88
    /**
89
     * @var string
90
     */
91
    public $schemaVersion = '3.0.6';
92
93
94
    /**
95
     * @var array The URIs for the element before it was saved
96
     */
97
    public $oldElementUris = [];
98
99
    // Public Methods
100
    // =========================================================================
101
102
    /**
103
     * @inheritdoc
104
     */
105
    public function init()
106
    {
107
        parent::init();
108
        self::$plugin = $this;
109
        // Initialize properties
110
        self::$settings = $this->getSettings();
111
        $this->name = self::$settings->pluginName;
112
        self::$cacheDuration = Craft::$app->getConfig()->getGeneral()->devMode
113
            ? $this::DEVMODE_CACHE_DURATION
114
            : null;
115
        // Handle any console commands
116
        $request = Craft::$app->getRequest();
117
        if ($request->getIsConsoleRequest()) {
118
            $this->controllerNamespace = 'nystudio107\retour\console\controllers';
119
        }
120
        // Install our event listeners
121
        $this->installEventListeners();
122
        // Log that Retour has been loaded
123
        Craft::info(
124
            Craft::t(
125
                'retour',
126
                '{name} plugin loaded',
127
                ['name' => $this->name]
128
            ),
129
            __METHOD__
130
        );
131
    }
132
133
    /**
134
     * @inheritdoc
135
     */
136
    public function getSettingsResponse()
137
    {
138
        // Just redirect to the plugin settings page
139
        Craft::$app->getResponse()->redirect(UrlHelper::cpUrl('retour/settings'));
140
    }
141
142
    /**
143
     * @inheritdoc
144
     */
145
    public function getCpNavItem()
146
    {
147
        $subNavs = [];
148
        $navItem = parent::getCpNavItem();
149
        $currentUser = Craft::$app->getUser()->getIdentity();
150
        // Only show sub-navs the user has permission to view
151
        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

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

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

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