Passed
Push — develop ( 9005d1...6a4cbf )
by Andrew
04:30
created

Redirects::saveRedirect()   C

Complexity

Conditions 11
Paths 73

Size

Total Lines 95
Code Lines 65

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 65
dl 0
loc 95
rs 6.6169
c 0
b 0
f 0
cc 11
nc 73
nop 1

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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\models\StaticRedirects as StaticRedirectsModel;
16
17
use Craft;
18
use craft\base\Component;
19
use craft\base\Plugin;
20
use craft\db\Query;
21
use craft\errors\SiteNotFoundException;
22
use craft\helpers\Db;
23
use craft\helpers\UrlHelper;
24
25
use yii\base\ExitException;
26
use yii\base\InvalidConfigException;
27
use yii\base\InvalidRouteException;
28
use yii\caching\TagDependency;
29
use yii\db\Exception;
30
use yii\web\HttpException;
31
32
/** @noinspection MissingPropertyAnnotationsInspection */
33
34
/**
35
 * @author    nystudio107
36
 * @package   Retour
37
 * @since     3.0.0
38
 */
39
class Redirects extends Component
40
{
41
    // Constants
42
    // =========================================================================
43
44
    const CACHE_KEY = 'retour_redirect_';
45
46
    const GLOBAL_REDIRECTS_CACHE_TAG = 'retour_redirects';
47
48
    // Protected Properties
49
    // =========================================================================
50
51
    /**
52
     * @var null|array
53
     */
54
    protected $cachedStaticRedirects;
55
56
    // Public Methods
57
    // =========================================================================
58
59
    /**
60
     * Handle 404s by looking for redirects
61
     */
62
    public function handle404()
63
    {
64
        Craft::info(
65
            Craft::t(
66
                'retour',
67
                'A 404 exception occurred'
68
            ),
69
            __METHOD__
70
        );
71
        $request = Craft::$app->getRequest();
72
        // We only want site requests that are not live preview or console requests
73
        if ($request->getIsSiteRequest() && !$request->getIsLivePreview() && !$request->getIsConsoleRequest()) {
74
            // See if we should redirect
75
            try {
76
                $fullUrl = urldecode($request->getAbsoluteUrl());
77
                $pathOnly = urldecode($request->getUrl());
78
            } catch (InvalidConfigException $e) {
79
                Craft::error(
80
                    $e->getMessage(),
81
                    __METHOD__
82
                );
83
                $pathOnly = '';
84
                $fullUrl = '';
85
            }
86
            // Strip the query string if `alwaysStripQueryString` is set
87
            if (Retour::$settings->alwaysStripQueryString) {
88
                $fullUrl = UrlHelper::stripQueryString($fullUrl);
89
                $pathOnly = UrlHelper::stripQueryString($pathOnly);
90
            }
91
            Craft::info(
92
                Craft::t(
93
                    'retour',
94
                    '404 full URL: {fullUrl}, 404 path only: {pathOnly}',
95
                    ['fullUrl' => $fullUrl, 'pathOnly' => $pathOnly]
96
                ),
97
                __METHOD__
98
            );
99
            // Redirect if we find a match, otherwise let Craft handle it
100
            $redirect = $this->findRedirectMatch($fullUrl, $pathOnly);
101
            if (!$this->doRedirect($fullUrl, $pathOnly, $redirect) && !Retour::$settings->alwaysStripQueryString) {
102
                // Try it again without the query string
103
                $fullUrl = UrlHelper::stripQueryString($fullUrl);
104
                $pathOnly = UrlHelper::stripQueryString($pathOnly);
105
                $redirect = $this->findRedirectMatch($fullUrl, $pathOnly);
106
                $this->doRedirect($fullUrl, $pathOnly, $redirect);
107
            }
108
            // Increment the stats
109
            Retour::$plugin->statistics->incrementStatistics($pathOnly, false);
110
        }
111
    }
112
113
    /**
114
     * Do the redirect
115
     *
116
     * @param string     $fullUrl
117
     * @param string     $pathOnly
118
     * @param null|array $redirect
119
     *
120
     * @return bool false if not redirected
121
     */
122
    public function doRedirect(string $fullUrl, string $pathOnly, $redirect): bool
123
    {
124
        $response = Craft::$app->getResponse();
125
        if ($redirect !== null) {
126
            // Figure out what type of source matching was done
127
            $redirectSrcMatch = $redirect['redirectSrcMatch'] ?? 'pathonly';
128
            switch ($redirectSrcMatch) {
129
                case 'pathonly':
130
                    $url = $pathOnly;
131
                    break;
132
                case 'fullurl':
133
                    $url = $fullUrl;
134
                    break;
135
                default:
136
                    $url = $pathOnly;
137
                    break;
138
            }
139
            $dest = $redirect['redirectDestUrl'];
140
            if (Retour::$settings->preserveQueryString) {
141
                $request = Craft::$app->getRequest();
142
                $dest .= '?' . $request->getQueryString();
143
            }
144
            $status = $redirect['redirectHttpCode'];
145
            Craft::info(
146
                Craft::t(
147
                    'retour',
148
                    'Redirecting {url} to {dest} with status {status}',
149
                    ['url' => $url, 'dest' => $dest, 'status' => $status]
150
                ),
151
                __METHOD__
152
            );
153
            // Increment the stats
154
            Retour::$plugin->statistics->incrementStatistics($url, true);
155
            // Handle a Retour return status > 400 to render the actual error template
156
            if ($status >= 400) {
157
                Retour::$currentException->statusCode = $status;
158
                $errorHandler = Craft::$app->getErrorHandler();
159
                $errorHandler->exception = Retour::$currentException;
160
                try {
161
                    $response = Craft::$app->runAction('templates/render-error');
162
                } catch (InvalidRouteException $e) {
163
                    Craft::error($e->getMessage(), __METHOD__);
164
                } catch (\yii\console\Exception $e) {
165
                    Craft::error($e->getMessage(), __METHOD__);
166
                }
167
            }
168
            // Redirect the request away;
169
            $response->redirect($dest, $status)->send();
170
            try {
171
                Craft::$app->end();
172
            } catch (ExitException $e) {
173
                Craft::error($e->getMessage(), __METHOD__);
174
            }
175
        }
176
177
        return false;
178
    }
179
180
    /**
181
     * @param string $fullUrl
182
     * @param string $pathOnly
183
     *
184
     * @return array|null
185
     */
186
    public function findRedirectMatch(string $fullUrl, string $pathOnly)
187
    {
188
        // Try getting the full URL redirect from the cache
189
        $redirect = $this->getRedirectFromCache($fullUrl);
190
        if ($redirect) {
191
            $this->incrementRedirectHitCount($redirect);
192
            $this->saveRedirectToCache($fullUrl, $redirect);
193
194
            return $redirect;
195
        }
196
197
        // Try getting the path only redirect from the cache
198
        $redirect = $this->getRedirectFromCache($pathOnly);
199
        if ($redirect) {
200
            $this->incrementRedirectHitCount($redirect);
201
            $this->saveRedirectToCache($pathOnly, $redirect);
202
203
            return $redirect;
204
        }
205
206
        // Resolve static redirects
207
        $redirects = $this->getAllStaticRedirects();
208
        $redirect = $this->resolveRedirect($fullUrl, $pathOnly, $redirects);
209
        if ($redirect) {
210
            return $redirect;
211
        }
212
213
        return null;
214
    }
215
216
    /**
217
     * @param $url
218
     *
219
     * @return bool|array
220
     */
221
    public function getRedirectFromCache($url)
222
    {
223
        $cache = Craft::$app->getCache();
224
        $cacheKey = $this::CACHE_KEY.md5($url);
225
        $redirect = $cache->get($cacheKey);
226
        Craft::info(
227
            Craft::t(
228
                'retour',
229
                'Cached redirect hit for {url}',
230
                ['url' => $url]
231
            ),
232
            __METHOD__
233
        );
234
235
        return $redirect;
236
    }
237
238
    /**
239
     * @param  string $url
240
     * @param  array  $redirect
241
     */
242
    public function saveRedirectToCache($url, $redirect)
243
    {
244
        $cacheKey = $this::CACHE_KEY.md5($url);
245
        $cache = Craft::$app->getCache();
246
        // Get the current site id
247
        $sites = Craft::$app->getSites();
248
        try {
249
            $siteId = $sites->getCurrentSite()->id;
250
        } catch (SiteNotFoundException $e) {
251
            $siteId = 1;
252
        }
253
        // Create the dependency tags
254
        $dependency = new TagDependency([
255
            'tags' => [
256
                $this::GLOBAL_REDIRECTS_CACHE_TAG,
257
                $this::GLOBAL_REDIRECTS_CACHE_TAG.$siteId,
258
            ],
259
        ]);
260
        $cache->set($cacheKey, $redirect, Retour::$cacheDuration, $dependency);
261
        Craft::info(
262
            Craft::t(
263
                'retour',
264
                'Cached redirect saved for {url}',
265
                ['url' => $url]
266
            ),
267
            __METHOD__
268
        );
269
    }
270
271
    /**
272
     * @param string $fullUrl
273
     * @param string $pathOnly
274
     * @param array  $redirects
275
     *
276
     * @return array|null
277
     */
278
    public function resolveRedirect(string $fullUrl, string $pathOnly, array $redirects)
279
    {
280
        $result = null;
281
        foreach ($redirects as $redirect) {
282
            // Figure out what type of source matching to do
283
            $redirectSrcMatch = $redirect['redirectSrcMatch'] ?? 'pathonly';
284
            $redirectEnabled = (bool)$redirect['enabled'];
285
            if ($redirectEnabled === true) {
286
                switch ($redirectSrcMatch) {
287
                    case 'pathonly':
288
                        $url = $pathOnly;
289
                        break;
290
                    case 'fullurl':
291
                        $url = $fullUrl;
292
                        break;
293
                    default:
294
                        $url = $pathOnly;
295
                        break;
296
                }
297
                $redirectMatchType = $redirect['redirectMatchType'] ?? 'notfound';
298
                switch ($redirectMatchType) {
299
                    // Do a straight up match
300
                    case 'exactmatch':
301
                        if (strcasecmp($redirect['redirectSrcUrlParsed'], $url) === 0) {
302
                            $this->incrementRedirectHitCount($redirect);
303
                            $this->saveRedirectToCache($url, $redirect);
304
305
                            return $redirect;
306
                        }
307
                        break;
308
309
                    // Do a regex match
310
                    case 'regexmatch':
311
                        $matchRegEx = '`'.$redirect['redirectSrcUrlParsed'].'`i';
312
                        if (preg_match($matchRegEx, $url) === 1) {
313
                            $this->incrementRedirectHitCount($redirect);
314
                            // If we're not associated with an EntryID, handle capture group replacement
315
                            if ((int)$redirect['associatedElementId'] === 0) {
316
                                $redirect['redirectDestUrl'] = preg_replace(
317
                                    $matchRegEx,
318
                                    $redirect['redirectDestUrl'],
319
                                    $url
320
                                );
321
                            }
322
                            $this->saveRedirectToCache($url, $redirect);
323
324
                            return $redirect;
325
                        }
326
                        break;
327
328
                    // Otherwise try to look up a plugin's method by and call it for the match
329
                    default:
330
                        $plugin = $redirectMatchType ? Craft::$app->getPlugins()->getPlugin($redirectMatchType) : null;
331
                        if ($plugin && method_exists($plugin, 'retourMatch')) {
332
                            $args = [
333
                                [
334
                                    'redirect' => &$redirect,
335
                                ],
336
                            ];
337
                            $result = \call_user_func_array([$plugin, 'retourMatch'], $args);
338
                            if ($result) {
339
                                $this->incrementRedirectHitCount($redirect);
340
                                $this->saveRedirectToCache($url, $redirect);
341
342
                                return $redirect;
343
                            }
344
                        }
345
                        break;
346
                }
347
            }
348
        }
349
        Craft::info(
350
            Craft::t(
351
                'retour',
352
                'Not handled-> full URL: {fullUrl}, path only: {pathOnly}',
353
                ['fullUrl' => $fullUrl, 'pathOnly' => $pathOnly]
354
            ),
355
            __METHOD__
356
        );
357
358
        return $result;
359
    }
360
361
    /**
362
     * Returns the list of matching schemes
363
     *
364
     * @return  array
365
     */
366
    public function getMatchesList(): array
367
    {
368
        $result = [
369
            'exactmatch' => Craft::t('retour', 'Exact Match'),
370
            'regexmatch' => Craft::t('retour', 'RegEx Match'),
371
        ];
372
373
        // Add any plugins that offer the retourMatch() method
374
        foreach (Craft::$app->getPlugins()->getAllPlugins() as $plugin) {
375
            /** @var Plugin $plugin */
376
            if (method_exists($plugin, 'retourMatch')) {
377
                $result[$plugin->getHandle()] = $plugin->name.Craft::t('retour', ' Match');
378
            }
379
        }
380
381
        return $result;
382
    }
383
384
    /**
385
     * @param null|int $limit
386
     *
387
     * @return array All of the statistics
388
     */
389
    public function getAllStaticRedirects($limit = null): array
390
    {
391
        // Cache it in our class; no need to fetch it more than once
392
        if ($this->cachedStaticRedirects !== null) {
393
            return $this->cachedStaticRedirects;
394
        }
395
        // Query the db table
396
        $query = (new Query())
397
            ->from(['{{%retour_static_redirects}}'])
398
            ->orderBy('redirectMatchType ASC, redirectSrcMatch ASC, hitCount DESC');
399
        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...
400
            $query->limit($limit);
401
        }
402
        $redirects = $query->all();
403
        // Cache for future accesses
404
        $this->cachedStaticRedirects = $redirects;
405
406
        return $redirects;
407
    }
408
409
    /**
410
     * Return a redirect by id
411
     *
412
     * @param int $id
413
     *
414
     * @return null|array The static redirect
415
     */
416
    public function getRedirectById(int $id)
417
    {
418
        // Query the db table
419
        $redirect = (new Query())
420
            ->from(['{{%retour_static_redirects}}'])
421
            ->where(['id' => $id])
422
            ->one();
423
424
        return $redirect;
425
    }
426
427
    /**
428
     * Return a redirect by redirectSrcUrl
429
     *
430
     * @param string $redirectSrcUrl
431
     *
432
     * @return null|array
433
     */
434
    public function getRedirectByRedirectSrcUrl(string $redirectSrcUrl)
435
    {
436
        // Query the db table
437
        $redirect = (new Query())
438
            ->from(['{{%retour_static_redirects}}'])
439
            ->where(['redirectSrcUrl' => $redirectSrcUrl])
440
            ->one();
441
442
        return $redirect;
443
    }
444
445
    /**
446
     * Delete a redirect by id
447
     *
448
     * @param int $id
449
     *
450
     * @return int The result
451
     */
452
    public function deleteRedirectById(int $id): int
453
    {
454
        $db = Craft::$app->getDb();
455
        // Delete a row from the db table
456
        try {
457
            $result = $db->createCommand()->delete(
458
                '{{%retour_static_redirects}}',
459
                [
460
                    'id' => $id,
461
                ]
462
            )->execute();
463
        } catch (Exception $e) {
464
            Craft::error($e->getMessage(), __METHOD__);
465
            $result = 0;
466
        }
467
468
        return $result;
469
    }
470
471
    /**
472
     * Increment the retour_static_redirects record
473
     *
474
     * @param $redirectConfig
475
     */
476
    public function incrementRedirectHitCount(&$redirectConfig)
477
    {
478
        if ($redirectConfig !== null) {
479
            $db = Craft::$app->getDb();
480
            $redirectConfig['hitCount']++;
481
            $redirectConfig['hitLastTime'] = Db::prepareDateForDb(new \DateTime());
482
            Craft::debug(
483
                Craft::t(
484
                    'retour',
485
                    'Incrementing statistics for: {redirect}',
486
                    ['redirect' => print_r($redirectConfig, true)]
487
                ),
488
                __METHOD__
489
            );
490
            // Update the existing record
491
            try {
492
                $rowsAffected = $db->createCommand()->update(
493
                    '{{%retour_static_redirects}}',
494
                    [
495
                        'hitCount' => $redirectConfig['hitCount'],
496
                        'hitLastTime' => $redirectConfig['hitLastTime'],
497
                    ],
498
                    [
499
                        'id' => $redirectConfig['id'],
500
                    ]
501
                )->execute();
502
                Craft::debug('Rows affected: '.$rowsAffected, __METHOD__);
503
            } catch (Exception $e) {
504
                Craft::error($e->getMessage(), __METHOD__);
505
            }
506
        }
507
    }
508
509
    /**
510
     * @param array $redirectConfig
511
     */
512
    public function saveRedirect(array $redirectConfig)
513
    {
514
        // Validate the model before saving it to the db
515
        $redirect = new StaticRedirectsModel($redirectConfig);
516
        if ($redirect->validate() === false) {
517
            Craft::error(
518
                Craft::t(
519
                    'retour',
520
                    'Error validating redirect {id}: {errors}',
521
                    ['id' => $redirect->id, 'errors' => print_r($redirect->getErrors(), true)]
522
                ),
523
                __METHOD__
524
            );
525
526
            return;
527
        }
528
        // Get the validated model attributes and save them to the db
529
        $redirectConfig = $redirect->getAttributes();
530
        // 0 for a siteId needs to be converted to null
531
        if (empty($redirectConfig['siteId']) || $redirectConfig['siteId'] == 0) {
532
            $redirectConfig['siteId'] = null;
533
        }
534
        $db = Craft::$app->getDb();
535
        // See if a redirect exists with this source URL already
536
        if ((int)$redirectConfig['id'] === 0) {
537
            // Query the db table
538
            $redirect = (new Query())
539
                ->from(['{{%retour_static_redirects}}'])
540
                ->where(['redirectSrcUrlParsed' => $redirectConfig['redirectSrcUrlParsed']])
541
                ->one();
542
            // If it exists, update it rather than having duplicates
543
            if (!empty($redirect)) {
544
                $redirectConfig['id'] = $redirect['id'];
545
            }
546
        }
547
        if ((int)$redirectConfig['id'] !== 0) {
548
            Craft::debug(
549
                Craft::t(
550
                    'retour',
551
                    'Updating existing redirect: {redirect}',
552
                    ['redirect' => print_r($redirectConfig, true)]
553
                ),
554
                __METHOD__
555
            );
556
            // Update the existing record
557
            try {
558
                $db->createCommand()->update(
559
                    '{{%retour_static_redirects}}',
560
                    $redirectConfig,
561
                    [
562
                        'id' => $redirectConfig['id'],
563
                    ]
564
                )->execute();
565
            } catch (Exception $e) {
566
                Craft::error($e->getMessage(), __METHOD__);
567
            }
568
        } else {
569
            Craft::debug(
570
                Craft::t(
571
                    'retour',
572
                    'Creating new redirect: {redirect}',
573
                    ['redirect' => print_r($redirectConfig, true)]
574
                ),
575
                __METHOD__
576
            );
577
            unset($redirectConfig['id']);
578
            // Create a new record
579
            try {
580
                $db->createCommand()->insert(
581
                    '{{%retour_static_redirects}}',
582
                    $redirectConfig
583
                )->execute();
584
            } catch (Exception $e) {
585
                Craft::error($e->getMessage(), __METHOD__);
586
            }
587
        }
588
        // To prevent redirect loops, see if any static redirects have our redirectDestUrl as their redirectSrcUrl
589
        $testRedirectConfig = $this->getRedirectByRedirectSrcUrl($redirectConfig['redirectDestUrl']);
590
        if ($testRedirectConfig !== null) {
591
            Craft::debug(
592
                Craft::t(
593
                    'retour',
594
                    'Deleting redirect to prevent a loop: {redirect}',
595
                    ['redirect' => print_r($testRedirectConfig, true)]
596
                ),
597
                __METHOD__
598
            );
599
            // Delete the redirect that has a redirectSrcUrl the same as this record's redirectDestUrl
600
            try {
601
                $db->createCommand()->delete(
602
                    '{{%retour_static_redirects}}',
603
                    ['id' => $testRedirectConfig['id']]
604
                )->execute();
605
            } catch (Exception $e) {
606
                Craft::error($e->getMessage(), __METHOD__);
607
            }
608
        }
609
    }
610
611
    /**
612
     * Invalidate all of the redirects caches
613
     */
614
    public function invalidateCaches()
615
    {
616
        $cache = Craft::$app->getCache();
617
        TagDependency::invalidate($cache, $this::GLOBAL_REDIRECTS_CACHE_TAG);
618
        Craft::info(
619
            Craft::t(
620
                'retour',
621
                'All redirect caches cleared'
622
            ),
623
            __METHOD__
624
        );
625
    }
626
}
627