Passed
Push — v3 ( a19b86...5f7589 )
by Andrew
25:32 queued 05:19
created

src/services/Redirects.php (1 issue)

Labels
Severity
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\services;
13
14
use nystudio107\retour\Retour;
15
use nystudio107\retour\events\RedirectEvent;
16
use nystudio107\retour\events\ResolveRedirectEvent;
17
use nystudio107\retour\models\StaticRedirects as StaticRedirectsModel;
18
19
use Craft;
20
use craft\base\Component;
21
use craft\base\Plugin;
22
use craft\db\Query;
23
use craft\errors\SiteNotFoundException;
24
use craft\helpers\Db;
25
use craft\helpers\UrlHelper;
26
27
use yii\base\ExitException;
28
use yii\base\InvalidConfigException;
29
use yii\base\InvalidRouteException;
30
use yii\caching\TagDependency;
31
use yii\db\Exception;
32
33
/** @noinspection MissingPropertyAnnotationsInspection */
34
35
/**
36
 * @author    nystudio107
37
 * @package   Retour
38
 * @since     3.0.0
39
 */
40
class Redirects extends Component
41
{
42
    // Constants
43
    // =========================================================================
44
45
    const CACHE_KEY = 'retour_redirect_';
46
47
    const GLOBAL_REDIRECTS_CACHE_TAG = 'retour_redirects';
48
49
    const EVENT_REDIRECT_ID = 0;
50
51
    /**
52
     * @event RedirectEvent The event that is triggered before the redirect is saved
53
     * You may set [[RedirectEvent::isValid]] to `false` to prevent the redirect from getting saved.
54
     *
55
     * ```php
56
     * use nystudio107\retour\services\Redirects;
57
     * use nystudio107\retour\events\RedirectEvent;
58
     *
59
     * Event::on(Redirects::class,
60
     *     Redirects::EVENT_BEFORE_SAVE_REDIRECT,
61
     *     function(RedirectEvent $event) {
62
     *         // potentially set $event->isValid;
63
     *     }
64
     * );
65
     * ```
66
     */
67
    const EVENT_BEFORE_SAVE_REDIRECT = 'beforeSaveRedirect';
68
69
    /**
70
     * @event RedirectEvent The event that is triggered after the redirect is saved
71
     *
72
     * ```php
73
     * use nystudio107\retour\services\Redirects;
74
     * use nystudio107\retour\events\RedirectEvent;
75
     *
76
     * Event::on(Redirects::class,
77
     *     Redirects::EVENT_AFTER_SAVE_REDIRECT,
78
     *     function(RedirectEvent $event) {
79
     *         // the redirect was saved
80
     *     }
81
     * );
82
     * ```
83
     */
84
    const EVENT_AFTER_SAVE_REDIRECT = 'afterSaveRedirect';
85
86
    /**
87
     * @event ResolveRedirectEvent The event that is triggered before Retour has attempted
88
     *        to resolve redirects. You may set [[ResolveRedirectEvent::redirectDestUrl]] to
89
     *        to the URL that it should redirect to, or null if no redirect should happen
90
     *
91
     * ```php
92
     * use nystudio107\retour\services\Redirects;
93
     * use nystudio107\retour\events\ResolveRedirectEvent;
94
     *
95
     * Event::on(Redirects::class,
96
     *     Redirects::EVENT_AFTER_SAVE_REDIRECT,
97
     *     function(ResolveRedirectEvent $event) {
98
     *         // potentially set $event->redirectDestUrl;
99
     *     }
100
     * );
101
     * ```
102
     */
103
    const EVENT_BEFORE_RESOLVE_REDIRECT = 'beforeResolveRedirect';
104
105
    /**
106
     * @event ResolveRedirectEvent The event that is triggered after Retour has attempted
107
     *        to resolve redirects. You may set [[ResolveRedirectEvent::redirectDestUrl]] to
108
     *        to the URL that it should redirect to, or null if no redirect should happen
109
     *
110
     * ```php
111
     * use nystudio107\retour\services\Redirects;
112
     * use nystudio107\retour\events\ResolveRedirectEvent;
113
     *
114
     * Event::on(Redirects::class,
115
     *     Redirects::EVENT_AFTER_RESOLVE_REDIRECT,
116
     *     function(ResolveRedirectEvent $event) {
117
     *         // potentially set $event->redirectDestUrl;
118
     *     }
119
     * );
120
     * ```
121
     */
122
    const EVENT_AFTER_RESOLVE_REDIRECT = 'afterResolveRedirect';
123
124
    // Protected Properties
125
    // =========================================================================
126
127
    /**
128
     * @var null|array
129
     */
130
    protected $cachedStaticRedirects;
131
132
    // Public Methods
133
    // =========================================================================
134
135
    /**
136
     * Handle 404s by looking for redirects
137
     */
138
    public function handle404()
139
    {
140
        Craft::info(
141
            Craft::t(
142
                'retour',
143
                'A 404 exception occurred'
144
            ),
145
            __METHOD__
146
        );
147
        $request = Craft::$app->getRequest();
148
        // We only want site requests that are not live preview or console requests
149
        if ($request->getIsSiteRequest() && !$request->getIsLivePreview() && !$request->getIsConsoleRequest()) {
150
            // See if we should redirect
151
            try {
152
                $fullUrl = urldecode($request->getAbsoluteUrl());
153
                $pathOnly = urldecode($request->getUrl());
154
            } catch (InvalidConfigException $e) {
155
                Craft::error(
156
                    $e->getMessage(),
157
                    __METHOD__
158
                );
159
                $pathOnly = '';
160
                $fullUrl = '';
161
            }
162
            // Strip the query string if `alwaysStripQueryString` is set
163
            if (Retour::$settings->alwaysStripQueryString) {
164
                $fullUrl = UrlHelper::stripQueryString($fullUrl);
165
                $pathOnly = UrlHelper::stripQueryString($pathOnly);
166
            }
167
            Craft::info(
168
                Craft::t(
169
                    'retour',
170
                    '404 full URL: {fullUrl}, 404 path only: {pathOnly}',
171
                    ['fullUrl' => $fullUrl, 'pathOnly' => $pathOnly]
172
                ),
173
                __METHOD__
174
            );
175
            if (!$this->excludeUri($pathOnly)) {
176
                // Redirect if we find a match, otherwise let Craft handle it
177
                $redirect = $this->findRedirectMatch($fullUrl, $pathOnly);
178
                if (!$this->doRedirect($fullUrl, $pathOnly, $redirect) && !Retour::$settings->alwaysStripQueryString) {
179
                    // Try it again without the query string
180
                    $fullUrl = UrlHelper::stripQueryString($fullUrl);
181
                    $pathOnly = UrlHelper::stripQueryString($pathOnly);
182
                    $redirect = $this->findRedirectMatch($fullUrl, $pathOnly);
183
                    $this->doRedirect($fullUrl, $pathOnly, $redirect);
184
                }
185
                // Increment the stats
186
                Retour::$plugin->statistics->incrementStatistics($pathOnly, false);
187
            }
188
        }
189
    }
190
191
    /**
192
     * Do the redirect
193
     *
194
     * @param string     $fullUrl
195
     * @param string     $pathOnly
196
     * @param null|array $redirect
197
     *
198
     * @return bool false if not redirected
199
     */
200
    public function doRedirect(string $fullUrl, string $pathOnly, $redirect): bool
201
    {
202
        $response = Craft::$app->getResponse();
203
        if ($redirect !== null) {
204
            // Figure out what type of source matching was done
205
            $redirectSrcMatch = $redirect['redirectSrcMatch'] ?? 'pathonly';
206
            switch ($redirectSrcMatch) {
207
                case 'pathonly':
208
                    $url = $pathOnly;
209
                    break;
210
                case 'fullurl':
211
                    $url = $fullUrl;
212
                    break;
213
                default:
214
                    $url = $pathOnly;
215
                    break;
216
            }
217
            $dest = $redirect['redirectDestUrl'];
218
            // If this isn't a full URL, make it one based on the appropriate site
219
            if (!UrlHelper::isFullUrl($dest)) {
220
                try {
221
                    $dest = UrlHelper::siteUrl($dest, null, null, $redirect['siteId'] ?? null);
222
                } catch (\yii\base\Exception $e) {
223
                }
224
            }
225
            if (Retour::$settings->preserveQueryString) {
226
                $request = Craft::$app->getRequest();
227
                if (!empty($request->getQueryStringWithoutPath())) {
228
                    $dest .= '?' . $request->getQueryStringWithoutPath();
229
                }
230
            }
231
            $redirectMatchType = $redirect['redirectMatchType'] ?? 'notfound';
232
            // Parse reference tags for exact matches
233
            if ($redirectMatchType === 'exactmatch') {
234
                $dest = Craft::$app->elements->parseRefs($dest, $redirect['siteId'] ?? null);
235
            }
236
            $status = $redirect['redirectHttpCode'];
237
            Craft::info(
238
                Craft::t(
239
                    'retour',
240
                    'Redirecting {url} to {dest} with status {status}',
241
                    ['url' => $url, 'dest' => $dest, 'status' => $status]
242
                ),
243
                __METHOD__
244
            );
245
            // Increment the stats
246
            Retour::$plugin->statistics->incrementStatistics($url, true);
247
            // Handle a Retour return status > 400 to render the actual error template
248
            if ($status >= 400) {
249
                Retour::$currentException->statusCode = $status;
250
                $errorHandler = Craft::$app->getErrorHandler();
251
                $errorHandler->exception = Retour::$currentException;
252
                try {
253
                    $response = Craft::$app->runAction('templates/render-error');
254
                } catch (InvalidRouteException $e) {
255
                    Craft::error($e->getMessage(), __METHOD__);
256
                } catch (\yii\console\Exception $e) {
257
                    Craft::error($e->getMessage(), __METHOD__);
258
                }
259
            }
260
            // Redirect the request away;
261
            $response->redirect($dest, $status)->send();
262
            try {
263
                Craft::$app->end();
264
            } catch (ExitException $e) {
265
                Craft::error($e->getMessage(), __METHOD__);
266
            }
267
        }
268
269
        return false;
270
    }
271
272
    /**
273
     * @param string $fullUrl
274
     * @param string $pathOnly
275
     * @param null   $siteId
276
     *
277
     * @return array|null
278
     */
279
    public function findRedirectMatch(string $fullUrl, string $pathOnly, $siteId = null)
280
    {
281
        // Get the current site
282
        if ($siteId === null) {
283
            $currentSite = Craft::$app->getSites()->currentSite;
284
            if ($currentSite) {
285
                $siteId = $currentSite->id;
286
            }
287
        }
288
        // Try getting the full URL redirect from the cache
289
        $redirect = $this->getRedirectFromCache($fullUrl, $siteId);
0 ignored issues
show
It seems like $siteId can also be of type null; however, parameter $siteId of nystudio107\retour\servi...:getRedirectFromCache() 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

289
        $redirect = $this->getRedirectFromCache($fullUrl, /** @scrutinizer ignore-type */ $siteId);
Loading history...
290
        if ($redirect) {
291
            $this->incrementRedirectHitCount($redirect);
292
            $this->saveRedirectToCache($fullUrl, $redirect);
293
294
            return $redirect;
295
        }
296
        // Try getting the path only redirect from the cache
297
        $redirect = $this->getRedirectFromCache($pathOnly, $siteId);
298
        if ($redirect) {
299
            $this->incrementRedirectHitCount($redirect);
300
            $this->saveRedirectToCache($pathOnly, $redirect);
301
302
            return $redirect;
303
        }
304
        // Resolve static redirects
305
        $redirects = $this->getAllStaticRedirects(null, $siteId);
306
        $redirect = $this->resolveRedirect($fullUrl, $pathOnly, $redirects);
307
        if ($redirect) {
308
            return $redirect;
309
        }
310
311
        return null;
312
    }
313
314
    /**
315
     * @param          $url
316
     * @param int|null $siteId
317
     *
318
     * @return bool|array
319
     */
320
    public function getRedirectFromCache($url, int $siteId = 0)
321
    {
322
        $cache = Craft::$app->getCache();
323
        $cacheKey = $this::CACHE_KEY.md5($url).$siteId;
324
        $redirect = $cache->get($cacheKey);
325
        Craft::info(
326
            Craft::t(
327
                'retour',
328
                'Cached redirect hit for {url}',
329
                ['url' => $url]
330
            ),
331
            __METHOD__
332
        );
333
334
        return $redirect;
335
    }
336
337
    /**
338
     * @param  string $url
339
     * @param  array  $redirect
340
     */
341
    public function saveRedirectToCache($url, $redirect)
342
    {
343
        $cache = Craft::$app->getCache();
344
        // Get the current site id
345
        $sites = Craft::$app->getSites();
346
        try {
347
            $siteId = $sites->getCurrentSite()->id;
348
        } catch (SiteNotFoundException $e) {
349
            $siteId = 1;
350
        }
351
        $cacheKey = $this::CACHE_KEY.md5($url).$siteId;
352
        // Create the dependency tags
353
        $dependency = new TagDependency([
354
            'tags' => [
355
                $this::GLOBAL_REDIRECTS_CACHE_TAG,
356
                $this::GLOBAL_REDIRECTS_CACHE_TAG.$siteId,
357
            ],
358
        ]);
359
        $cache->set($cacheKey, $redirect, Retour::$cacheDuration, $dependency);
360
        Craft::info(
361
            Craft::t(
362
                'retour',
363
                'Cached redirect saved for {url}',
364
                ['url' => $url]
365
            ),
366
            __METHOD__
367
        );
368
    }
369
370
    /**
371
     * @param string $fullUrl
372
     * @param string $pathOnly
373
     * @param array  $redirects
374
     *
375
     * @return array|null
376
     */
377
    public function resolveRedirect(string $fullUrl, string $pathOnly, array $redirects)
378
    {
379
        $result = null;
380
        // Throw the Redirects::EVENT_BEFORE_RESOLVE_REDIRECT event
381
        $event = new ResolveRedirectEvent([
382
            'fullUrl' => $fullUrl,
383
            'pathOnly' => $pathOnly,
384
            'redirectDestUrl' => null,
385
            'redirectHttpCode' => 301,
386
        ]);
387
        $this->trigger(self::EVENT_BEFORE_RESOLVE_REDIRECT, $event);
388
        if ($event->redirectDestUrl !== null) {
389
            return $this->resolveEventRedirect($event);
390
        }
391
        // Iterate through the redirects
392
        foreach ($redirects as $redirect) {
393
            // Figure out what type of source matching to do
394
            $redirectSrcMatch = $redirect['redirectSrcMatch'] ?? 'pathonly';
395
            $redirectEnabled = (bool)$redirect['enabled'];
396
            if ($redirectEnabled === true) {
397
                switch ($redirectSrcMatch) {
398
                    case 'pathonly':
399
                        $url = $pathOnly;
400
                        break;
401
                    case 'fullurl':
402
                        $url = $fullUrl;
403
                        break;
404
                    default:
405
                        $url = $pathOnly;
406
                        break;
407
                }
408
                $redirectMatchType = $redirect['redirectMatchType'] ?? 'notfound';
409
                switch ($redirectMatchType) {
410
                    // Do a straight up match
411
                    case 'exactmatch':
412
                        if (strcasecmp($redirect['redirectSrcUrlParsed'], $url) === 0) {
413
                            $this->incrementRedirectHitCount($redirect);
414
                            $this->saveRedirectToCache($url, $redirect);
415
416
                            return $redirect;
417
                        }
418
                        break;
419
420
                    // Do a regex match
421
                    case 'regexmatch':
422
                        $matchRegEx = '`'.$redirect['redirectSrcUrlParsed'].'`i';
423
                        try {
424
                            if (preg_match($matchRegEx, $url) === 1) {
425
                                $this->incrementRedirectHitCount($redirect);
426
                                // If we're not associated with an EntryID, handle capture group replacement
427
                                if ((int)$redirect['associatedElementId'] === 0) {
428
                                    $redirect['redirectDestUrl'] = preg_replace(
429
                                        $matchRegEx,
430
                                        $redirect['redirectDestUrl'],
431
                                        $url
432
                                    );
433
                                }
434
                                $this->saveRedirectToCache($url, $redirect);
435
436
                                return $redirect;
437
                            }
438
                        } catch (\Exception $e) {
439
                            // That's fine
440
                            Craft::error('Invalid Redirect Regex: '.$matchRegEx, __METHOD__);
441
                        }
442
443
                        break;
444
445
                    // Otherwise try to look up a plugin's method by and call it for the match
446
                    default:
447
                        $plugin = $redirectMatchType ? Craft::$app->getPlugins()->getPlugin($redirectMatchType) : null;
448
                        if ($plugin && method_exists($plugin, 'retourMatch')) {
449
                            $args = [
450
                                [
451
                                    'redirect' => &$redirect,
452
                                ],
453
                            ];
454
                            $result = \call_user_func_array([$plugin, 'retourMatch'], $args);
455
                            if ($result) {
456
                                $this->incrementRedirectHitCount($redirect);
457
                                $this->saveRedirectToCache($url, $redirect);
458
459
                                return $redirect;
460
                            }
461
                        }
462
                        break;
463
                }
464
            }
465
        }
466
        // Throw the Redirects::EVENT_AFTER_RESOLVE_REDIRECT event
467
        $event = new ResolveRedirectEvent([
468
            'fullUrl' => $fullUrl,
469
            'pathOnly' => $pathOnly,
470
            'redirectDestUrl' => null,
471
            'redirectHttpCode' => 301,
472
        ]);
473
        $this->trigger(self::EVENT_AFTER_RESOLVE_REDIRECT, $event);
474
        if ($event->redirectDestUrl !== null) {
475
            return $this->resolveEventRedirect($event);
476
        }
477
        Craft::info(
478
            Craft::t(
479
                'retour',
480
                'Not handled-> full URL: {fullUrl}, path only: {pathOnly}',
481
                ['fullUrl' => $fullUrl, 'pathOnly' => $pathOnly]
482
            ),
483
            __METHOD__
484
        );
485
486
        return $result;
487
    }
488
489
    /**
490
     * @param ResolveRedirectEvent $event
491
     *
492
     * @return null|array
493
     */
494
    public function resolveEventRedirect(ResolveRedirectEvent $event)
495
    {
496
        $result = null;
497
498
        if ($event->redirectDestUrl !== null) {
499
            $redirect = new StaticRedirectsModel([
500
                'id' => self::EVENT_REDIRECT_ID,
501
                'redirectDestUrl' => $event->redirectDestUrl,
502
                'redirectHttpCode' => $event->redirectHttpCode,
503
            ]);
504
            $result = $redirect->toArray();
505
        }
506
507
        return $result;
508
    }
509
510
    /**
511
     * Returns the list of matching schemes
512
     *
513
     * @return  array
514
     */
515
    public function getMatchesList(): array
516
    {
517
        $result = [
518
            'exactmatch' => Craft::t('retour', 'Exact Match'),
519
            'regexmatch' => Craft::t('retour', 'RegEx Match'),
520
        ];
521
522
        // Add any plugins that offer the retourMatch() method
523
        foreach (Craft::$app->getPlugins()->getAllPlugins() as $plugin) {
524
            /** @var Plugin $plugin */
525
            if (method_exists($plugin, 'retourMatch')) {
526
                $result[$plugin->getHandle()] = $plugin->name.Craft::t('retour', ' Match');
527
            }
528
        }
529
530
        return $result;
531
    }
532
533
    /**
534
     * @param null|int $limit
535
     * @param int|null $siteId
536
     *
537
     * @return array All of the statistics
538
     */
539
    public function getAllStaticRedirects($limit = null, int $siteId = null): array
540
    {
541
        // Cache it in our class; no need to fetch it more than once
542
        if ($this->cachedStaticRedirects !== null) {
543
            return $this->cachedStaticRedirects;
544
        }
545
        // Query the db table
546
        $query = (new Query())
547
            ->from(['{{%retour_static_redirects}}'])
548
            ->orderBy('redirectMatchType ASC, redirectSrcMatch ASC, hitCount DESC');
549
        if ($siteId) {
550
            $query
551
                ->where(['siteId' => $siteId])
552
                ->orWhere(['siteId' => null]);
553
        }
554
        if ($limit) {
555
            $query->limit($limit);
556
        }
557
        $redirects = $query->all();
558
        // Cache for future accesses
559
        $this->cachedStaticRedirects = $redirects;
560
561
        return $redirects;
562
    }
563
564
    /**
565
     * Return a redirect by id
566
     *
567
     * @param int $id
568
     *
569
     * @return null|array The static redirect
570
     */
571
    public function getRedirectById(int $id)
572
    {
573
        // Query the db table
574
        $redirect = (new Query())
575
            ->from(['{{%retour_static_redirects}}'])
576
            ->where(['id' => $id])
577
            ->one();
578
579
        return $redirect;
580
    }
581
582
    /**
583
     * Return a redirect by redirectSrcUrl
584
     *
585
     * @param string   $redirectSrcUrl
586
     * @param int|null $siteId
587
     *
588
     * @return null|array
589
     */
590
    public function getRedirectByRedirectSrcUrl(string $redirectSrcUrl, int $siteId = null)
591
    {
592
        // Query the db table
593
        $query = (new Query())
594
            ->from(['{{%retour_static_redirects}}'])
595
            ->where(['redirectSrcUrl' => $redirectSrcUrl])
596
            ;
597
        if ($siteId) {
598
            $query
599
                ->andWhere(['or', [
600
                    'siteId' => $siteId,
601
                ], [
602
                    'siteId' => null,
603
                ]]);
604
        }
605
        $redirect = $query->one();
606
607
        return $redirect;
608
    }
609
610
    /**
611
     * Delete a redirect by id
612
     *
613
     * @param int $id
614
     *
615
     * @return int The result
616
     */
617
    public function deleteRedirectById(int $id): int
618
    {
619
        $db = Craft::$app->getDb();
620
        // Delete a row from the db table
621
        try {
622
            $result = $db->createCommand()->delete(
623
                '{{%retour_static_redirects}}',
624
                [
625
                    'id' => $id,
626
                ]
627
            )->execute();
628
        } catch (Exception $e) {
629
            Craft::error($e->getMessage(), __METHOD__);
630
            $result = 0;
631
        }
632
633
        return $result;
634
    }
635
636
    /**
637
     * Increment the retour_static_redirects record
638
     *
639
     * @param $redirectConfig
640
     */
641
    public function incrementRedirectHitCount(&$redirectConfig)
642
    {
643
        if ($redirectConfig !== null) {
644
            $db = Craft::$app->getDb();
645
            $redirectConfig['hitCount']++;
646
            $redirectConfig['hitLastTime'] = Db::prepareDateForDb(new \DateTime());
647
            Craft::debug(
648
                Craft::t(
649
                    'retour',
650
                    'Incrementing statistics for: {redirect}',
651
                    ['redirect' => print_r($redirectConfig, true)]
652
                ),
653
                __METHOD__
654
            );
655
            // Update the existing record
656
            try {
657
                $rowsAffected = $db->createCommand()->update(
658
                    '{{%retour_static_redirects}}',
659
                    [
660
                        'hitCount' => $redirectConfig['hitCount'],
661
                        'hitLastTime' => $redirectConfig['hitLastTime'],
662
                    ],
663
                    [
664
                        'id' => $redirectConfig['id'],
665
                    ]
666
                )->execute();
667
                Craft::debug('Rows affected: '.$rowsAffected, __METHOD__);
668
            } catch (\Exception $e) {
669
                Craft::error($e->getMessage(), __METHOD__);
670
            }
671
        }
672
    }
673
674
    /**
675
     * @param array $redirectConfig
676
     */
677
    public function saveRedirect(array $redirectConfig)
678
    {
679
        // Validate the model before saving it to the db
680
        $redirect = new StaticRedirectsModel($redirectConfig);
681
        if ($redirect->validate() === false) {
682
            Craft::error(
683
                Craft::t(
684
                    'retour',
685
                    'Error validating redirect {id}: {errors}',
686
                    ['id' => $redirect->id, 'errors' => print_r($redirect->getErrors(), true)]
687
                ),
688
                __METHOD__
689
            );
690
691
            return;
692
        }
693
        // Get the validated model attributes and save them to the db
694
        $redirectConfig = $redirect->getAttributes();
695
        // 0 for a siteId needs to be converted to null
696
        if (empty($redirectConfig['siteId']) || (int)$redirectConfig['siteId'] === 0) {
697
            $redirectConfig['siteId'] = null;
698
        }
699
        // Throw an event to before saving the redirect
700
        $db = Craft::$app->getDb();
701
        // See if a redirect exists with this source URL already
702
        if ((int)$redirectConfig['id'] === 0) {
703
            // Query the db table
704
            $redirect = (new Query())
705
                ->from(['{{%retour_static_redirects}}'])
706
                ->where(['redirectSrcUrlParsed' => $redirectConfig['redirectSrcUrlParsed']])
707
                ->andWhere(['siteId' => $redirectConfig['siteId']])
708
                ->one();
709
            // If it exists, update it rather than having duplicates
710
            if (!empty($redirect)) {
711
                $redirectConfig['id'] = $redirect['id'];
712
            }
713
        }
714
        // Trigger a 'beforeSaveRedirect' event
715
        $isNew = (int)$redirectConfig['id'] === 0;
716
        $event = new RedirectEvent([
717
            'isNew' => $isNew,
718
            'legacyUrl' => $redirectConfig['redirectSrcUrlParsed'],
719
            'destinationUrl' => $redirectConfig['redirectDestUrl'],
720
            'matchType' => $redirectConfig['redirectSrcMatch'],
721
            'redirectType' => $redirectConfig['redirectHttpCode'],
722
        ]);
723
        $this->trigger(self::EVENT_BEFORE_SAVE_REDIRECT, $event);
724
        if (!$event->isValid) {
725
            return;
726
        }
727
        // See if this is an existing redirect
728
        if (!$isNew) {
729
            Craft::debug(
730
                Craft::t(
731
                    'retour',
732
                    'Updating existing redirect: {redirect}',
733
                    ['redirect' => print_r($redirectConfig, true)]
734
                ),
735
                __METHOD__
736
            );
737
            // Update the existing record
738
            try {
739
                $db->createCommand()->update(
740
                    '{{%retour_static_redirects}}',
741
                    $redirectConfig,
742
                    [
743
                        'id' => $redirectConfig['id'],
744
                    ]
745
                )->execute();
746
            } catch (Exception $e) {
747
                Craft::error($e->getMessage(), __METHOD__);
748
            }
749
        } else {
750
            Craft::debug(
751
                Craft::t(
752
                    'retour',
753
                    'Creating new redirect: {redirect}',
754
                    ['redirect' => print_r($redirectConfig, true)]
755
                ),
756
                __METHOD__
757
            );
758
            unset($redirectConfig['id']);
759
            // Create a new record
760
            try {
761
                $db->createCommand()->insert(
762
                    '{{%retour_static_redirects}}',
763
                    $redirectConfig
764
                )->execute();
765
            } catch (Exception $e) {
766
                Craft::error($e->getMessage(), __METHOD__);
767
            }
768
        }
769
        // To prevent redirect loops, see if any static redirects have our redirectDestUrl as their redirectSrcUrl
770
        $testRedirectConfig = $this->getRedirectByRedirectSrcUrl(
771
            $redirectConfig['redirectDestUrl'],
772
            $redirectConfig['siteId']
773
        );
774
        if ($testRedirectConfig !== null) {
775
            Craft::debug(
776
                Craft::t(
777
                    'retour',
778
                    'Deleting redirect to prevent a loop: {redirect}',
779
                    ['redirect' => print_r($testRedirectConfig, true)]
780
                ),
781
                __METHOD__
782
            );
783
            // Delete the redirect that has a redirectSrcUrl the same as this record's redirectDestUrl
784
            try {
785
                $db->createCommand()->delete(
786
                    '{{%retour_static_redirects}}',
787
                    ['id' => $testRedirectConfig['id']]
788
                )->execute();
789
            } catch (Exception $e) {
790
                Craft::error($e->getMessage(), __METHOD__);
791
            }
792
        }
793
        // Trigger a 'afterSaveRedirect' event
794
        $this->trigger(self::EVENT_AFTER_SAVE_REDIRECT, $event);
795
    }
796
797
    /**
798
     * Invalidate all of the redirects caches
799
     */
800
    public function invalidateCaches()
801
    {
802
        $cache = Craft::$app->getCache();
803
        TagDependency::invalidate($cache, $this::GLOBAL_REDIRECTS_CACHE_TAG);
804
        Craft::info(
805
            Craft::t(
806
                'retour',
807
                'All redirect caches cleared'
808
            ),
809
            __METHOD__
810
        );
811
    }
812
813
    /**
814
     * @param $uri
815
     *
816
     * @return bool
817
     */
818
    public function excludeUri($uri): bool
819
    {
820
        $uri = '/'.ltrim($uri, '/');
821
        if (!empty(Retour::$settings->excludePatterns)) {
822
            foreach (Retour::$settings->excludePatterns as $excludePattern) {
823
                $pattern = '`'.$excludePattern['pattern'].'`i';
824
                try {
825
                    if (preg_match($pattern, $uri) === 1) {
826
                        return true;
827
                    }
828
                } catch (\Exception $e) {
829
                    // That's fine
830
                    Craft::error('Invalid exclude URI Regex: '.$pattern, __METHOD__);
831
                }
832
            }
833
        }
834
835
        return false;
836
    }
837
}
838