Passed
Push — develop ( cee39f...4b73a5 )
by Andrew
04:44
created

Redirects::resolveEventRedirect()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 2
c 1
b 0
f 0
dl 0
loc 5
rs 10
cc 1
nc 1
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
    /**
50
     * @event RedirectEvent The event that is triggered before the redirect is saved
51
     * You may set [[RedirectEvent::isValid]] to `false` to prevent the redirect from getting saved.
52
     *
53
     * ```php
54
     * use nystudio107\retour\services\Redirects;
55
     * use nystudio107\retour\events\RedirectEvent;
56
     *
57
     * Event::on(Redirects::class,
58
     *     Redirects::EVENT_BEFORE_SAVE_REDIRECT,
59
     *     function(RedirectEvent $event) {
60
     *         // potentially set $event->isValid;
61
     *     }
62
     * );
63
     * ```
64
     */
65
    const EVENT_BEFORE_SAVE_REDIRECT = 'beforeSaveRedirect';
66
67
    /**
68
     * @event RedirectEvent The event that is triggered after the redirect is saved
69
     *
70
     * ```php
71
     * use nystudio107\retour\services\Redirects;
72
     * use nystudio107\retour\events\RedirectEvent;
73
     *
74
     * Event::on(Redirects::class,
75
     *     Redirects::EVENT_AFTER_SAVE_REDIRECT,
76
     *     function(RedirectEvent $event) {
77
     *         // the redirect was saved
78
     *     }
79
     * );
80
     * ```
81
     */
82
    const EVENT_AFTER_SAVE_REDIRECT = 'afterSaveRedirect';
83
84
    /**
85
     * @event ResolveRedirectEvent The event that is triggered before Retour has attempted
86
     *        to resolve redirects. You may set [[ResolveRedirectEvent::redirectDestUrl]] to
87
     *        to the URL that it should redirect to, or null if no redirect should happen
88
     *
89
     * ```php
90
     * use nystudio107\retour\services\Redirects;
91
     * use nystudio107\retour\events\ResolveRedirectEvent;
92
     *
93
     * Event::on(Redirects::class,
94
     *     Redirects::EVENT_AFTER_SAVE_REDIRECT,
95
     *     function(ResolveRedirectEvent $event) {
96
     *         // potentially set $event->redirectDestUrl;
97
     *     }
98
     * );
99
     * ```
100
     */
101
    const EVENT_BEFORE_RESOLVE_REDIRECT = 'beforeResolveRedirect';
102
103
    /**
104
     * @event ResolveRedirectEvent The event that is triggered after Retour has attempted
105
     *        to resolve redirects. You may set [[ResolveRedirectEvent::redirectDestUrl]] to
106
     *        to the URL that it should redirect to, or null if no redirect should happen
107
     *
108
     * ```php
109
     * use nystudio107\retour\services\Redirects;
110
     * use nystudio107\retour\events\ResolveRedirectEvent;
111
     *
112
     * Event::on(Redirects::class,
113
     *     Redirects::EVENT_AFTER_RESOLVE_REDIRECT,
114
     *     function(ResolveRedirectEvent $event) {
115
     *         // potentially set $event->redirectDestUrl;
116
     *     }
117
     * );
118
     * ```
119
     */
120
    const EVENT_AFTER_RESOLVE_REDIRECT = 'afterResolveRedirect';
121
122
    // Protected Properties
123
    // =========================================================================
124
125
    /**
126
     * @var null|array
127
     */
128
    protected $cachedStaticRedirects;
129
130
    // Public Methods
131
    // =========================================================================
132
133
    /**
134
     * Handle 404s by looking for redirects
135
     */
136
    public function handle404()
137
    {
138
        Craft::info(
139
            Craft::t(
140
                'retour',
141
                'A 404 exception occurred'
142
            ),
143
            __METHOD__
144
        );
145
        $request = Craft::$app->getRequest();
146
        // We only want site requests that are not live preview or console requests
147
        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 ( Ignorable by Annotation )

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

147
        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 ( Ignorable by Annotation )

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

147
        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...
148
            // See if we should redirect
149
            try {
150
                $fullUrl = urldecode($request->getAbsoluteUrl());
151
                $pathOnly = urldecode($request->getUrl());
152
            } catch (InvalidConfigException $e) {
153
                Craft::error(
154
                    $e->getMessage(),
155
                    __METHOD__
156
                );
157
                $pathOnly = '';
158
                $fullUrl = '';
159
            }
160
            // Strip the query string if `alwaysStripQueryString` is set
161
            if (Retour::$settings->alwaysStripQueryString) {
162
                $fullUrl = UrlHelper::stripQueryString($fullUrl);
163
                $pathOnly = UrlHelper::stripQueryString($pathOnly);
164
            }
165
            Craft::info(
166
                Craft::t(
167
                    'retour',
168
                    '404 full URL: {fullUrl}, 404 path only: {pathOnly}',
169
                    ['fullUrl' => $fullUrl, 'pathOnly' => $pathOnly]
170
                ),
171
                __METHOD__
172
            );
173
            if (!$this->excludeUri($pathOnly)) {
174
                // Redirect if we find a match, otherwise let Craft handle it
175
                $redirect = $this->findRedirectMatch($fullUrl, $pathOnly);
176
                if (!$this->doRedirect($fullUrl, $pathOnly, $redirect) && !Retour::$settings->alwaysStripQueryString) {
177
                    // Try it again without the query string
178
                    $fullUrl = UrlHelper::stripQueryString($fullUrl);
179
                    $pathOnly = UrlHelper::stripQueryString($pathOnly);
180
                    $redirect = $this->findRedirectMatch($fullUrl, $pathOnly);
181
                    $this->doRedirect($fullUrl, $pathOnly, $redirect);
182
                }
183
                // Increment the stats
184
                Retour::$plugin->statistics->incrementStatistics($pathOnly, false);
185
            }
186
        }
187
    }
188
189
    /**
190
     * Do the redirect
191
     *
192
     * @param string     $fullUrl
193
     * @param string     $pathOnly
194
     * @param null|array $redirect
195
     *
196
     * @return bool false if not redirected
197
     */
198
    public function doRedirect(string $fullUrl, string $pathOnly, $redirect): bool
199
    {
200
        $response = Craft::$app->getResponse();
201
        if ($redirect !== null) {
202
            // Figure out what type of source matching was done
203
            $redirectSrcMatch = $redirect['redirectSrcMatch'] ?? 'pathonly';
204
            switch ($redirectSrcMatch) {
205
                case 'pathonly':
206
                    $url = $pathOnly;
207
                    break;
208
                case 'fullurl':
209
                    $url = $fullUrl;
210
                    break;
211
                default:
212
                    $url = $pathOnly;
213
                    break;
214
            }
215
            $dest = $redirect['redirectDestUrl'];
216
            // If this isn't a full URL, make it one based on the appropriate site
217
            if (!UrlHelper::isFullUrl($dest)) {
218
                try {
219
                    $dest = UrlHelper::siteUrl($dest, null, null, $redirect['siteId'] ?? null);
220
                } 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...
221
                }
222
            }
223
            if (Retour::$settings->preserveQueryString) {
224
                $request = Craft::$app->getRequest();
225
                if (!empty($request->getQueryStringWithoutPath())) {
226
                    $dest .= '?' . $request->getQueryStringWithoutPath();
227
                }
228
            }
229
            $status = $redirect['redirectHttpCode'];
230
            Craft::info(
231
                Craft::t(
232
                    'retour',
233
                    'Redirecting {url} to {dest} with status {status}',
234
                    ['url' => $url, 'dest' => $dest, 'status' => $status]
235
                ),
236
                __METHOD__
237
            );
238
            // Increment the stats
239
            Retour::$plugin->statistics->incrementStatistics($url, true);
240
            // Handle a Retour return status > 400 to render the actual error template
241
            if ($status >= 400) {
242
                Retour::$currentException->statusCode = $status;
243
                $errorHandler = Craft::$app->getErrorHandler();
244
                $errorHandler->exception = Retour::$currentException;
245
                try {
246
                    $response = Craft::$app->runAction('templates/render-error');
247
                } catch (InvalidRouteException $e) {
248
                    Craft::error($e->getMessage(), __METHOD__);
249
                } catch (\yii\console\Exception $e) {
250
                    Craft::error($e->getMessage(), __METHOD__);
251
                }
252
            }
253
            // Redirect the request away;
254
            $response->redirect($dest, $status)->send();
255
            try {
256
                Craft::$app->end();
257
            } catch (ExitException $e) {
258
                Craft::error($e->getMessage(), __METHOD__);
259
            }
260
        }
261
262
        return false;
263
    }
264
265
    /**
266
     * @param string $fullUrl
267
     * @param string $pathOnly
268
     * @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...
269
     *
270
     * @return array|null
271
     */
272
    public function findRedirectMatch(string $fullUrl, string $pathOnly, $siteId = null)
273
    {
274
        // Get the current site
275
        if ($siteId === null) {
0 ignored issues
show
introduced by
The condition $siteId === null is always true.
Loading history...
276
            $currentSite = Craft::$app->getSites()->currentSite;
277
            if ($currentSite) {
278
                $siteId = $currentSite->id;
279
            }
280
        }
281
        // Try getting the full URL redirect from the cache
282
        $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

282
        $redirect = $this->getRedirectFromCache($fullUrl, /** @scrutinizer ignore-type */ $siteId);
Loading history...
283
        if ($redirect) {
284
            $this->incrementRedirectHitCount($redirect);
285
            $this->saveRedirectToCache($fullUrl, $redirect);
286
287
            return $redirect;
288
        }
289
        // Try getting the path only redirect from the cache
290
        $redirect = $this->getRedirectFromCache($pathOnly, $siteId);
291
        if ($redirect) {
292
            $this->incrementRedirectHitCount($redirect);
293
            $this->saveRedirectToCache($pathOnly, $redirect);
294
295
            return $redirect;
296
        }
297
        // Resolve static redirects
298
        $redirects = $this->getAllStaticRedirects(null, $siteId);
299
        $redirect = $this->resolveRedirect($fullUrl, $pathOnly, $redirects);
300
        if ($redirect) {
301
            return $redirect;
302
        }
303
304
        return null;
305
    }
306
307
    /**
308
     * @param          $url
309
     * @param int|null $siteId
310
     *
311
     * @return bool|array
312
     */
313
    public function getRedirectFromCache($url, int $siteId = 0)
314
    {
315
        $cache = Craft::$app->getCache();
316
        $cacheKey = $this::CACHE_KEY.md5($url).$siteId;
317
        $redirect = $cache->get($cacheKey);
318
        Craft::info(
319
            Craft::t(
320
                'retour',
321
                'Cached redirect hit for {url}',
322
                ['url' => $url]
323
            ),
324
            __METHOD__
325
        );
326
327
        return $redirect;
328
    }
329
330
    /**
331
     * @param  string $url
332
     * @param  array  $redirect
333
     */
334
    public function saveRedirectToCache($url, $redirect)
335
    {
336
        $cache = Craft::$app->getCache();
337
        // Get the current site id
338
        $sites = Craft::$app->getSites();
339
        try {
340
            $siteId = $sites->getCurrentSite()->id;
341
        } catch (SiteNotFoundException $e) {
342
            $siteId = 1;
343
        }
344
        $cacheKey = $this::CACHE_KEY.md5($url).$siteId;
345
        // Create the dependency tags
346
        $dependency = new TagDependency([
347
            'tags' => [
348
                $this::GLOBAL_REDIRECTS_CACHE_TAG,
349
                $this::GLOBAL_REDIRECTS_CACHE_TAG.$siteId,
350
            ],
351
        ]);
352
        $cache->set($cacheKey, $redirect, Retour::$cacheDuration, $dependency);
353
        Craft::info(
354
            Craft::t(
355
                'retour',
356
                'Cached redirect saved for {url}',
357
                ['url' => $url]
358
            ),
359
            __METHOD__
360
        );
361
    }
362
363
    /**
364
     * @param string $fullUrl
365
     * @param string $pathOnly
366
     * @param array  $redirects
367
     *
368
     * @return array|null
369
     */
370
    public function resolveRedirect(string $fullUrl, string $pathOnly, array $redirects)
371
    {
372
        $result = null;
373
        // Throw the Redirects::EVENT_BEFORE_RESOLVE_REDIRECT event
374
        $event = new ResolveRedirectEvent([
375
            'fullUrl' => $fullUrl,
376
            'pathOnly' => $pathOnly,
377
            'redirectDestUrl' => null,
378
            'redirectHttpCode' => 301,
379
        ]);
380
        $this->trigger(self::EVENT_BEFORE_RESOLVE_REDIRECT, $event);
381
        if ($event->redirectDestUrl !== null) {
382
            return $this->resolveEventRedirect($event);
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->resolveEventRedirect($event) targeting nystudio107\retour\servi...:resolveEventRedirect() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
383
        }
384
        // Iterate through the redirects
385
        foreach ($redirects as $redirect) {
386
            // Figure out what type of source matching to do
387
            $redirectSrcMatch = $redirect['redirectSrcMatch'] ?? 'pathonly';
388
            $redirectEnabled = (bool)$redirect['enabled'];
389
            if ($redirectEnabled === true) {
390
                switch ($redirectSrcMatch) {
391
                    case 'pathonly':
392
                        $url = $pathOnly;
393
                        break;
394
                    case 'fullurl':
395
                        $url = $fullUrl;
396
                        break;
397
                    default:
398
                        $url = $pathOnly;
399
                        break;
400
                }
401
                $redirectMatchType = $redirect['redirectMatchType'] ?? 'notfound';
402
                switch ($redirectMatchType) {
403
                    // Do a straight up match
404
                    case 'exactmatch':
405
                        if (strcasecmp($redirect['redirectSrcUrlParsed'], $url) === 0) {
406
                            $this->incrementRedirectHitCount($redirect);
407
                            $this->saveRedirectToCache($url, $redirect);
408
409
                            return $redirect;
410
                        }
411
                        break;
412
413
                    // Do a regex match
414
                    case 'regexmatch':
415
                        $matchRegEx = '`'.$redirect['redirectSrcUrlParsed'].'`i';
416
                        if (preg_match($matchRegEx, $url) === 1) {
417
                            $this->incrementRedirectHitCount($redirect);
418
                            // If we're not associated with an EntryID, handle capture group replacement
419
                            if ((int)$redirect['associatedElementId'] === 0) {
420
                                $redirect['redirectDestUrl'] = preg_replace(
421
                                    $matchRegEx,
422
                                    $redirect['redirectDestUrl'],
423
                                    $url
424
                                );
425
                            }
426
                            $this->saveRedirectToCache($url, $redirect);
427
428
                            return $redirect;
429
                        }
430
                        break;
431
432
                    // Otherwise try to look up a plugin's method by and call it for the match
433
                    default:
434
                        $plugin = $redirectMatchType ? Craft::$app->getPlugins()->getPlugin($redirectMatchType) : null;
435
                        if ($plugin && method_exists($plugin, 'retourMatch')) {
436
                            $args = [
437
                                [
438
                                    'redirect' => &$redirect,
439
                                ],
440
                            ];
441
                            $result = \call_user_func_array([$plugin, 'retourMatch'], $args);
442
                            if ($result) {
443
                                $this->incrementRedirectHitCount($redirect);
444
                                $this->saveRedirectToCache($url, $redirect);
445
446
                                return $redirect;
447
                            }
448
                        }
449
                        break;
450
                }
451
            }
452
        }
453
        // Throw the Redirects::EVENT_AFTER_RESOLVE_REDIRECT event
454
        $event = new ResolveRedirectEvent([
455
            'fullUrl' => $fullUrl,
456
            'pathOnly' => $pathOnly,
457
            'redirectDestUrl' => null,
458
            'redirectHttpCode' => 301,
459
        ]);
460
        $this->trigger(self::EVENT_AFTER_RESOLVE_REDIRECT, $event);
461
        if ($event->redirectDestUrl !== null) {
462
            return $this->resolveEventRedirect($event);
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->resolveEventRedirect($event) targeting nystudio107\retour\servi...:resolveEventRedirect() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
463
        }
464
        Craft::info(
465
            Craft::t(
466
                'retour',
467
                'Not handled-> full URL: {fullUrl}, path only: {pathOnly}',
468
                ['fullUrl' => $fullUrl, 'pathOnly' => $pathOnly]
469
            ),
470
            __METHOD__
471
        );
472
473
        return $result;
474
    }
475
476
    /**
477
     * @param ResolveRedirectEvent $event
478
     *
479
     * @return null|array
480
     */
481
    public function resolveEventRedirect(ResolveRedirectEvent $event)
0 ignored issues
show
Unused Code introduced by
The parameter $event is not used and could be removed. ( Ignorable by Annotation )

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

481
    public function resolveEventRedirect(/** @scrutinizer ignore-unused */ ResolveRedirectEvent $event)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
482
    {
483
        $result = null;
484
485
        return $result;
486
    }
487
488
    /**
489
     * Returns the list of matching schemes
490
     *
491
     * @return  array
492
     */
493
    public function getMatchesList(): array
494
    {
495
        $result = [
496
            'exactmatch' => Craft::t('retour', 'Exact Match'),
497
            'regexmatch' => Craft::t('retour', 'RegEx Match'),
498
        ];
499
500
        // Add any plugins that offer the retourMatch() method
501
        foreach (Craft::$app->getPlugins()->getAllPlugins() as $plugin) {
502
            /** @var Plugin $plugin */
503
            if (method_exists($plugin, 'retourMatch')) {
504
                $result[$plugin->getHandle()] = $plugin->name.Craft::t('retour', ' Match');
505
            }
506
        }
507
508
        return $result;
509
    }
510
511
    /**
512
     * @param null|int $limit
513
     * @param int|null $siteId
514
     *
515
     * @return array All of the statistics
516
     */
517
    public function getAllStaticRedirects($limit = null, int $siteId = null): array
518
    {
519
        // Cache it in our class; no need to fetch it more than once
520
        if ($this->cachedStaticRedirects !== null) {
521
            return $this->cachedStaticRedirects;
522
        }
523
        // Query the db table
524
        $query = (new Query())
525
            ->from(['{{%retour_static_redirects}}'])
526
            ->orderBy('redirectMatchType ASC, redirectSrcMatch ASC, hitCount DESC');
527
        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...
528
            $query
529
                ->where(['siteId' => $siteId])
530
                ->orWhere(['siteId' => null]);
531
        }
532
        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...
533
            $query->limit($limit);
534
        }
535
        $redirects = $query->all();
536
        // Cache for future accesses
537
        $this->cachedStaticRedirects = $redirects;
538
539
        return $redirects;
540
    }
541
542
    /**
543
     * Return a redirect by id
544
     *
545
     * @param int $id
546
     *
547
     * @return null|array The static redirect
548
     */
549
    public function getRedirectById(int $id)
550
    {
551
        // Query the db table
552
        $redirect = (new Query())
553
            ->from(['{{%retour_static_redirects}}'])
554
            ->where(['id' => $id])
555
            ->one();
556
557
        return $redirect;
558
    }
559
560
    /**
561
     * Return a redirect by redirectSrcUrl
562
     *
563
     * @param string   $redirectSrcUrl
564
     * @param int|null $siteId
565
     *
566
     * @return null|array
567
     */
568
    public function getRedirectByRedirectSrcUrl(string $redirectSrcUrl, int $siteId = null)
569
    {
570
        // Query the db table
571
        $query = (new Query())
572
            ->from(['{{%retour_static_redirects}}'])
573
            ->where(['redirectSrcUrl' => $redirectSrcUrl])
574
            ;
575
        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...
576
            $query
577
                ->andWhere(['or', [
578
                    'siteId' => $siteId,
579
                ], [
580
                    'siteId' => null,
581
                ]]);
582
        }
583
        $redirect = $query->one();
584
585
        return $redirect;
586
    }
587
588
    /**
589
     * Delete a redirect by id
590
     *
591
     * @param int $id
592
     *
593
     * @return int The result
594
     */
595
    public function deleteRedirectById(int $id): int
596
    {
597
        $db = Craft::$app->getDb();
598
        // Delete a row from the db table
599
        try {
600
            $result = $db->createCommand()->delete(
601
                '{{%retour_static_redirects}}',
602
                [
603
                    'id' => $id,
604
                ]
605
            )->execute();
606
        } catch (Exception $e) {
607
            Craft::error($e->getMessage(), __METHOD__);
608
            $result = 0;
609
        }
610
611
        return $result;
612
    }
613
614
    /**
615
     * Increment the retour_static_redirects record
616
     *
617
     * @param $redirectConfig
618
     */
619
    public function incrementRedirectHitCount(&$redirectConfig)
620
    {
621
        if ($redirectConfig !== null) {
622
            $db = Craft::$app->getDb();
623
            $redirectConfig['hitCount']++;
624
            $redirectConfig['hitLastTime'] = Db::prepareDateForDb(new \DateTime());
625
            Craft::debug(
626
                Craft::t(
627
                    'retour',
628
                    'Incrementing statistics for: {redirect}',
629
                    ['redirect' => print_r($redirectConfig, true)]
630
                ),
631
                __METHOD__
632
            );
633
            // Update the existing record
634
            try {
635
                $rowsAffected = $db->createCommand()->update(
636
                    '{{%retour_static_redirects}}',
637
                    [
638
                        'hitCount' => $redirectConfig['hitCount'],
639
                        'hitLastTime' => $redirectConfig['hitLastTime'],
640
                    ],
641
                    [
642
                        'id' => $redirectConfig['id'],
643
                    ]
644
                )->execute();
645
                Craft::debug('Rows affected: '.$rowsAffected, __METHOD__);
646
            } catch (Exception $e) {
647
                Craft::error($e->getMessage(), __METHOD__);
648
            }
649
        }
650
    }
651
652
    /**
653
     * @param array $redirectConfig
654
     */
655
    public function saveRedirect(array $redirectConfig)
656
    {
657
        // Validate the model before saving it to the db
658
        $redirect = new StaticRedirectsModel($redirectConfig);
659
        if ($redirect->validate() === false) {
660
            Craft::error(
661
                Craft::t(
662
                    'retour',
663
                    'Error validating redirect {id}: {errors}',
664
                    ['id' => $redirect->id, 'errors' => print_r($redirect->getErrors(), true)]
665
                ),
666
                __METHOD__
667
            );
668
669
            return;
670
        }
671
        // Get the validated model attributes and save them to the db
672
        $redirectConfig = $redirect->getAttributes();
673
        // 0 for a siteId needs to be converted to null
674
        if (empty($redirectConfig['siteId']) || (int)$redirectConfig['siteId'] === 0) {
675
            $redirectConfig['siteId'] = null;
676
        }
677
        // Throw an event to before saving the redirect
678
        $db = Craft::$app->getDb();
679
        // See if a redirect exists with this source URL already
680
        if ((int)$redirectConfig['id'] === 0) {
681
            // Query the db table
682
            $redirect = (new Query())
683
                ->from(['{{%retour_static_redirects}}'])
684
                ->where(['redirectSrcUrlParsed' => $redirectConfig['redirectSrcUrlParsed']])
685
                ->andWhere(['siteId' => $redirectConfig['siteId']])
686
                ->one();
687
            // If it exists, update it rather than having duplicates
688
            if (!empty($redirect)) {
689
                $redirectConfig['id'] = $redirect['id'];
690
            }
691
        }
692
        // Trigger a 'beforeSaveRedirect' event
693
        $isNew = (int)$redirectConfig['id'] === 0;
694
        $event = new RedirectEvent([
695
            'isNew' => $isNew,
696
            'legacyUrl' => $redirectConfig['redirectSrcUrlParsed'],
697
            'destinationUrl' => $redirectConfig['redirectDestUrl'],
698
            'matchType' => $redirectConfig['redirectSrcMatch'],
699
            'redirectType' => $redirectConfig['redirectHttpCode'],
700
        ]);
701
        $this->trigger(self::EVENT_BEFORE_SAVE_REDIRECT, $event);
702
        if (!$event->isValid) {
703
            return;
704
        }
705
        // See if this is an existing redirect
706
        if (!$isNew) {
707
            Craft::debug(
708
                Craft::t(
709
                    'retour',
710
                    'Updating existing redirect: {redirect}',
711
                    ['redirect' => print_r($redirectConfig, true)]
712
                ),
713
                __METHOD__
714
            );
715
            // Update the existing record
716
            try {
717
                $db->createCommand()->update(
718
                    '{{%retour_static_redirects}}',
719
                    $redirectConfig,
720
                    [
721
                        'id' => $redirectConfig['id'],
722
                    ]
723
                )->execute();
724
            } catch (Exception $e) {
725
                Craft::error($e->getMessage(), __METHOD__);
726
            }
727
        } else {
728
            Craft::debug(
729
                Craft::t(
730
                    'retour',
731
                    'Creating new redirect: {redirect}',
732
                    ['redirect' => print_r($redirectConfig, true)]
733
                ),
734
                __METHOD__
735
            );
736
            unset($redirectConfig['id']);
737
            // Create a new record
738
            try {
739
                $db->createCommand()->insert(
740
                    '{{%retour_static_redirects}}',
741
                    $redirectConfig
742
                )->execute();
743
            } catch (Exception $e) {
744
                Craft::error($e->getMessage(), __METHOD__);
745
            }
746
        }
747
        // To prevent redirect loops, see if any static redirects have our redirectDestUrl as their redirectSrcUrl
748
        $testRedirectConfig = $this->getRedirectByRedirectSrcUrl(
749
            $redirectConfig['redirectDestUrl'],
750
            $redirectConfig['siteId']
751
        );
752
        if ($testRedirectConfig !== null) {
753
            Craft::debug(
754
                Craft::t(
755
                    'retour',
756
                    'Deleting redirect to prevent a loop: {redirect}',
757
                    ['redirect' => print_r($testRedirectConfig, true)]
758
                ),
759
                __METHOD__
760
            );
761
            // Delete the redirect that has a redirectSrcUrl the same as this record's redirectDestUrl
762
            try {
763
                $db->createCommand()->delete(
764
                    '{{%retour_static_redirects}}',
765
                    ['id' => $testRedirectConfig['id']]
766
                )->execute();
767
            } catch (Exception $e) {
768
                Craft::error($e->getMessage(), __METHOD__);
769
            }
770
        }
771
        // Trigger a 'afterSaveRedirect' event
772
        $this->trigger(self::EVENT_AFTER_SAVE_REDIRECT, $event);
773
    }
774
775
    /**
776
     * Invalidate all of the redirects caches
777
     */
778
    public function invalidateCaches()
779
    {
780
        $cache = Craft::$app->getCache();
781
        TagDependency::invalidate($cache, $this::GLOBAL_REDIRECTS_CACHE_TAG);
782
        Craft::info(
783
            Craft::t(
784
                'retour',
785
                'All redirect caches cleared'
786
            ),
787
            __METHOD__
788
        );
789
    }
790
791
    /**
792
     * @param $uri
793
     *
794
     * @return bool
795
     */
796
    public function excludeUri($uri): bool
797
    {
798
        $uri = '/'.ltrim($uri, '/');
799
        if (!empty(Retour::$settings->excludePatterns)) {
800
            foreach (Retour::$settings->excludePatterns as $excludePattern) {
801
                $pattern = '`'.$excludePattern['pattern'].'`i';
802
                if (preg_match($pattern, $uri) === 1) {
803
                    return true;
804
                }
805
            }
806
        }
807
808
        return false;
809
    }
810
}
811