Passed
Push — develop ( a5d855...47c333 )
by Andrew
09:28 queued 04:43
created

Redirects::excludeUri()   A

Complexity

Conditions 5
Paths 3

Size

Total Lines 18
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 3
Bugs 0 Features 0
Metric Value
eloc 10
c 3
b 0
f 0
dl 0
loc 18
rs 9.6111
cc 5
nc 3
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\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()) {
0 ignored issues
show
Deprecated Code introduced by
The function craft\console\Request::getIsLivePreview() has been deprecated: in 3.2.0 ( Ignorable by Annotation )

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

149
        if ($request->getIsSiteRequest() && !/** @scrutinizer ignore-deprecated */ $request->getIsLivePreview() && !$request->getIsConsoleRequest()) {

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
Deprecated Code introduced by
The function craft\web\Request::getIsLivePreview() has been deprecated: in 3.2.0 ( Ignorable by Annotation )

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

149
        if ($request->getIsSiteRequest() && !/** @scrutinizer ignore-deprecated */ $request->getIsLivePreview() && !$request->getIsConsoleRequest()) {

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
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) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
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
0 ignored issues
show
Documentation Bug introduced by
Are you sure the doc-type for parameter $siteId is correct as it would always require null to be passed?
Loading history...
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) {
0 ignored issues
show
introduced by
The condition $siteId === null is always true.
Loading history...
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
Bug introduced by
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) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $siteId of type integer|null is loosely compared to true; this is ambiguous if the integer can be 0. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
550
            $query
551
                ->where(['siteId' => $siteId])
552
                ->orWhere(['siteId' => null]);
553
        }
554
        if ($limit) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $limit of type integer|null is loosely compared to true; this is ambiguous if the integer can be 0. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
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) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $siteId of type integer|null is loosely compared to true; this is ambiguous if the integer can be 0. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
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