Passed
Branch v3 (3b6795)
by Andrew
07:36
created

Redirects::deleteRedirectById()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 17
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 0
Metric Value
eloc 10
dl 0
loc 17
ccs 0
cts 10
cp 0
rs 9.9332
c 0
b 0
f 0
cc 2
nc 2
nop 1
crap 6
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/
0 ignored issues
show
Coding Style introduced by
The tag in position 1 should be the @copyright tag
Loading history...
9
 * @copyright Copyright (c) 2018 nystudio107
0 ignored issues
show
Coding Style introduced by
@copyright tag must contain a year and the name of the copyright holder
Loading history...
10
 */
0 ignored issues
show
Coding Style introduced by
PHP version not specified
Loading history...
Coding Style introduced by
Missing @category tag in file comment
Loading history...
Coding Style introduced by
Missing @package tag in file comment
Loading history...
Coding Style introduced by
Missing @author tag in file comment
Loading history...
Coding Style introduced by
Missing @license tag in file comment
Loading history...
11
12
namespace nystudio107\retour\services;
13
14
use Craft;
15
use craft\base\Component;
16
use craft\base\ElementInterface;
17
use craft\base\Plugin;
18
use craft\db\Query;
19
use craft\errors\SiteNotFoundException;
20
use craft\helpers\Db;
21
use craft\helpers\ElementHelper;
22
use craft\helpers\StringHelper;
23
use nystudio107\retour\events\RedirectEvent;
24
use nystudio107\retour\events\RedirectResolvedEvent;
25
use nystudio107\retour\events\ResolveRedirectEvent;
26
use nystudio107\retour\fields\ShortLink;
27
use nystudio107\retour\helpers\UrlHelper;
28
use nystudio107\retour\models\StaticRedirects as StaticRedirectsModel;
29
use nystudio107\retour\Retour;
30
use yii\base\ExitException;
31
use yii\base\InvalidConfigException;
32
use yii\base\InvalidRouteException;
33
use yii\caching\TagDependency;
34
use yii\db\Exception;
35
36
/** @noinspection MissingPropertyAnnotationsInspection */
0 ignored issues
show
Coding Style introduced by
The open comment tag must be the only content on the line
Loading history...
Coding Style introduced by
The close comment tag must be the only content on the line
Loading history...
Coding Style introduced by
Missing short description in doc comment
Loading history...
37
38
/**
0 ignored issues
show
Coding Style introduced by
Missing short description in doc comment
Loading history...
39
 * @author    nystudio107
0 ignored issues
show
Coding Style introduced by
The tag in position 1 should be the @package tag
Loading history...
Coding Style introduced by
Content of the @author tag must be in the form "Display Name <[email protected]>"
Loading history...
Coding Style introduced by
Tag value for @author tag indented incorrectly; expected 2 spaces but found 4
Loading history...
40
 * @package   Retour
0 ignored issues
show
Coding Style introduced by
Tag value for @package tag indented incorrectly; expected 1 spaces but found 3
Loading history...
41
 * @since     3.0.0
0 ignored issues
show
Coding Style introduced by
The tag in position 3 should be the @author tag
Loading history...
Coding Style introduced by
Tag value for @since tag indented incorrectly; expected 3 spaces but found 5
Loading history...
42
 */
0 ignored issues
show
Coding Style introduced by
Missing @category tag in class comment
Loading history...
Coding Style introduced by
Missing @license tag in class comment
Loading history...
Coding Style introduced by
Missing @link tag in class comment
Loading history...
43
class Redirects extends Component
44
{
45
    // Constants
46
    // =========================================================================
47
48
    const CACHE_KEY = 'retour_redirect_';
49
50
    const GLOBAL_REDIRECTS_CACHE_TAG = 'retour_redirects';
51
52
    const EVENT_REDIRECT_ID = 0;
53
54
    /**
0 ignored issues
show
Coding Style introduced by
Missing short description in doc comment
Loading history...
55
     * @event RedirectEvent The event that is triggered before the redirect is saved
56
     * You may set [[RedirectEvent::isValid]] to `false` to prevent the redirect from getting saved.
57
     *
58
     * ```php
59
     * use nystudio107\retour\services\Redirects;
60
     * use nystudio107\retour\events\RedirectEvent;
61
     *
62
     * Event::on(Redirects::class,
63
     *     Redirects::EVENT_BEFORE_SAVE_REDIRECT,
64
     *     function(RedirectEvent $event) {
65
     *         // potentially set $event->isValid;
66
     *     }
67
     * );
68
     * ```
69
     */
70
    const EVENT_BEFORE_SAVE_REDIRECT = 'beforeSaveRedirect';
71
72
    /**
0 ignored issues
show
Coding Style introduced by
Missing short description in doc comment
Loading history...
73
     * @event RedirectEvent The event that is triggered after the redirect is saved
74
     *
75
     * ```php
76
     * use nystudio107\retour\services\Redirects;
77
     * use nystudio107\retour\events\RedirectEvent;
78
     *
79
     * Event::on(Redirects::class,
80
     *     Redirects::EVENT_AFTER_SAVE_REDIRECT,
81
     *     function(RedirectEvent $event) {
82
     *         // the redirect was saved
83
     *     }
84
     * );
85
     * ```
86
     */
87
    const EVENT_AFTER_SAVE_REDIRECT = 'afterSaveRedirect';
88
89
    /**
0 ignored issues
show
Coding Style introduced by
Missing short description in doc comment
Loading history...
90
     * @event ResolveRedirectEvent The event that is triggered before Retour has attempted
91
     *        to resolve redirects. You may set [[ResolveRedirectEvent::redirectDestUrl]] to
92
     *        to the URL that it should redirect to, or null if no redirect should happen
93
     *
94
     * ```php
95
     * use nystudio107\retour\services\Redirects;
96
     * use nystudio107\retour\events\ResolveRedirectEvent;
97
     *
98
     * Event::on(Redirects::class,
99
     *     Redirects::EVENT_AFTER_SAVE_REDIRECT,
100
     *     function(ResolveRedirectEvent $event) {
101
     *         // potentially set $event->redirectDestUrl;
102
     *     }
103
     * );
104
     * ```
105
     */
106
    const EVENT_BEFORE_RESOLVE_REDIRECT = 'beforeResolveRedirect';
107
108
    /**
0 ignored issues
show
Coding Style introduced by
Missing short description in doc comment
Loading history...
109
     * @event ResolveRedirectEvent The event that is triggered after Retour has attempted
110
     *        to resolve redirects. You may set [[ResolveRedirectEvent::redirectDestUrl]] to
111
     *        to the URL that it should redirect to, or null if no redirect should happen
112
     *
113
     * ```php
114
     * use nystudio107\retour\services\Redirects;
115
     * use nystudio107\retour\events\ResolveRedirectEvent;
116
     *
117
     * Event::on(Redirects::class,
118
     *     Redirects::EVENT_AFTER_RESOLVE_REDIRECT,
119
     *     function(ResolveRedirectEvent $event) {
120
     *         // potentially set $event->redirectDestUrl;
121
     *     }
122
     * );
123
     * ```
124
     */
125
    const EVENT_AFTER_RESOLVE_REDIRECT = 'afterResolveRedirect';
126
127
    /**
0 ignored issues
show
Coding Style introduced by
Missing short description in doc comment
Loading history...
128
     * @event RedirectResolvedEvent The event that is triggered once Retour has resolved
129
     *        a redirect. [[RedirectResolvedEvent::redirect]] will be set to the redirect
130
     *        that was resolved. You may set [[RedirectResolvedEvent::redirectDestUrl]] to
131
     *        to a different URL that it should redirect to, or leave it null if the
132
     *        redirect should happen as resolved.
133
     *
134
     * ```php
135
     * use nystudio107\retour\services\Redirects;
136
     * use nystudio107\retour\events\RedirectResolvedEvent;
137
     *
138
     * Event::on(Redirects::class,
139
     *     Redirects::EVENT_REDIRECT_RESOLVED,
140
     *     function(RedirectResolvedEvent $event) {
141
     *         // potentially set $event->redirectDestUrl;
142
     *     }
143
     * );
144
     * ```
145
     */
146
    const EVENT_REDIRECT_RESOLVED = 'redirectResolved';
147
148
    // Protected Properties
149
    // =========================================================================
150
151
    /**
0 ignored issues
show
Coding Style introduced by
Missing short description in doc comment
Loading history...
152
     * @var null|array
153
     */
154
    protected $cachedStaticRedirects;
155
156
    /**
0 ignored issues
show
Coding Style introduced by
Missing short description in doc comment
Loading history...
157
     * @var null|array
158
     */
159
    protected $cachedRegExRedirects;
160
161
    /**
0 ignored issues
show
Coding Style introduced by
Missing short description in doc comment
Loading history...
162
     * @var null|array
163
     */
164
    protected $cachedExactMatchRedirects;
165
166
    // Public Methods
167
    // =========================================================================
168
169
    /**
170
     * Handle 404s by looking for redirects
171
     */
0 ignored issues
show
Coding Style introduced by
Missing @return tag in function comment
Loading history...
172
    public function handle404()
173
    {
174
        Craft::info(
175
            Craft::t(
176
                'retour',
177
                'A 404 exception occurred'
178
            ),
179
            __METHOD__
180
        );
181
        $request = Craft::$app->getRequest();
182
        // We only want site requests that are not live preview or console requests
183
        if ($request->getIsSiteRequest() && !$this->isPreview($request) && !$request->getIsConsoleRequest()) {
184
            // See if we should redirect
185
            try {
186
                $fullUrl = urldecode($request->getAbsoluteUrl());
187
                $pathOnly = urldecode($request->getUrl());
188
            } catch (InvalidConfigException $e) {
189
                Craft::error(
190
                    $e->getMessage(),
191
                    __METHOD__
192
                );
193
                $pathOnly = '';
194
                $fullUrl = '';
195
            }
196
            // Strip the query string if `alwaysStripQueryString` is set
197
            if (Retour::$settings->alwaysStripQueryString) {
198
                $fullUrl = UrlHelper::stripQueryString($fullUrl);
199
                $pathOnly = UrlHelper::stripQueryString($pathOnly);
200
            }
201
            // Stash the $pathOnly for use when incrementing the statistics
202
            $originalPathOnly = $pathOnly;
203
            Craft::info(
204
                Craft::t(
205
                    'retour',
206
                    '404 full URL: {fullUrl}, 404 path only: {pathOnly}',
207
                    ['fullUrl' => $fullUrl, 'pathOnly' => $pathOnly]
208
                ),
209
                __METHOD__
210
            );
211
            if (!$this->excludeUri($pathOnly)) {
212
                // Redirect if we find a match, otherwise let Craft handle it
213
                $redirect = $this->findRedirectMatch($fullUrl, $pathOnly);
214
                if (!$this->doRedirect($fullUrl, $pathOnly, $redirect) && !Retour::$settings->alwaysStripQueryString) {
215
                    // Try it again without the query string
216
                    $fullUrl = UrlHelper::stripQueryString($fullUrl);
217
                    $pathOnly = UrlHelper::stripQueryString($pathOnly);
218
                    $redirect = $this->findRedirectMatch($fullUrl, $pathOnly);
219
                    $this->doRedirect($fullUrl, $pathOnly, $redirect);
220
                }
221
                // Increment the stats
222
                Retour::$plugin->statistics->incrementStatistics($originalPathOnly, false);
223
            }
224
        }
225
    }
226
227
    /**
228
     * Do the redirect
229
     *
230
     * @param string $fullUrl
0 ignored issues
show
Coding Style introduced by
Expected 5 spaces after parameter type; 1 found
Loading history...
Coding Style introduced by
Missing parameter comment
Loading history...
231
     * @param string $pathOnly
0 ignored issues
show
Coding Style introduced by
Expected 5 spaces after parameter type; 1 found
Loading history...
Coding Style introduced by
Missing parameter comment
Loading history...
232
     * @param null|array $redirect
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
233
     *
234
     * @return bool false if not redirected
235
     */
236
    public function doRedirect(string $fullUrl, string $pathOnly, $redirect): bool
237
    {
238
        $response = Craft::$app->getResponse();
239
        if ($redirect !== null) {
240
            // Figure out what type of source matching was done
241
            $redirectSrcMatch = $redirect['redirectSrcMatch'] ?? 'pathonly';
242
            switch ($redirectSrcMatch) {
243
                case 'pathonly':
0 ignored issues
show
Coding Style introduced by
Line indented incorrectly; expected 12 spaces, found 16
Loading history...
244
                    $url = $pathOnly;
245
                    break;
246
                case 'fullurl':
0 ignored issues
show
Coding Style introduced by
Line indented incorrectly; expected 12 spaces, found 16
Loading history...
247
                    $url = $fullUrl;
248
                    break;
249
                default:
0 ignored issues
show
Coding Style introduced by
Line indented incorrectly; expected 12 spaces, found 16
Loading history...
250
                    $url = $pathOnly;
251
                    break;
252
            }
253
            $dest = $redirect['redirectDestUrl'];
254
            // If this isn't a full URL, make it one based on the appropriate site
255
            if (!UrlHelper::isFullUrl($dest)) {
256
                try {
257
                    $siteId = $redirect['siteId'] ?? null;
258
                    if ($siteId !== null) {
259
                        $siteId = (int)$siteId;
260
                    }
261
                    $dest = UrlHelper::siteUrl($dest, null, null, $siteId);
262
                } 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...
263
                }
264
            }
265
            if (Retour::$settings->preserveQueryString) {
266
                $request = Craft::$app->getRequest();
267
                $queryString = '';
0 ignored issues
show
Unused Code introduced by
The assignment to $queryString is dead and can be removed.
Loading history...
268
                try {
269
                    $queryString = UrlHelper::combineQueryStringsFromUrls($dest, $request->getUrl());
270
                } catch (InvalidConfigException $e) {
271
                    // That's ok
272
                }
273
                if (!empty($queryString)) {
274
                    $dest = strtok($dest, '?') . '?' . $queryString;
275
                }
276
            }
277
            $redirectMatchType = $redirect['redirectMatchType'] ?? 'notfound';
278
            // Parse reference tags for exact matches
279
            if ($redirectMatchType === 'exactmatch') {
280
                $dest = Craft::$app->elements->parseRefs($dest, $redirect['siteId'] ?? null);
0 ignored issues
show
Bug introduced by
The method parseRefs() does not exist on null. ( Ignorable by Annotation )

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

280
                /** @scrutinizer ignore-call */ 
281
                $dest = Craft::$app->elements->parseRefs($dest, $redirect['siteId'] ?? null);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
281
            }
282
            $status = $redirect['redirectHttpCode'];
283
            Craft::info(
284
                Craft::t(
285
                    'retour',
286
                    'Redirecting {url} to {dest} with status {status}',
287
                    ['url' => $url, 'dest' => $dest, 'status' => $status]
288
                ),
289
                __METHOD__
290
            );
291
            // Increment the stats
292
            Retour::$plugin->statistics->incrementStatistics($url, true);
293
            // Handle a Retour return status > 400 to render the actual error template
294
            if ($status >= 400) {
295
                Retour::$currentException->statusCode = $status;
296
                $errorHandler = Craft::$app->getErrorHandler();
297
                $errorHandler->exception = Retour::$currentException;
298
                try {
299
                    $response = Craft::$app->runAction('templates/render-error');
300
                } catch (InvalidRouteException $e) {
301
                    Craft::error($e->getMessage(), __METHOD__);
302
                } catch (\yii\console\Exception $e) {
303
                    Craft::error($e->getMessage(), __METHOD__);
304
                }
305
            }
306
            // Sanitize the URL
307
            $dest = UrlHelper::sanitizeUrl($dest);
308
            // Add any additional headers (existing ones will be replaced)
309
            if (!empty(Retour::$settings->additionalHeaders)) {
310
                foreach (Retour::$settings->additionalHeaders as $additionalHeader) {
311
                    $response->headers->set($additionalHeader['name'], $additionalHeader['value']);
312
                }
313
            }
314
            // Redirect the request away;
315
            $response->redirect($dest, $status)->send();
316
            try {
317
                Craft::$app->end();
318
            } catch (ExitException $e) {
319
                Craft::error($e->getMessage(), __METHOD__);
320
            }
321
        }
322
323
        return false;
324
    }
325
326
    /**
0 ignored issues
show
Coding Style introduced by
Missing short description in doc comment
Loading history...
327
     * @param string $fullUrl
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
328
     * @param string $pathOnly
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
329
     * @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...
Coding Style introduced by
Missing parameter comment
Loading history...
Coding Style introduced by
Expected 3 spaces after parameter type; 1 found
Loading history...
330
     *
331
     * @return array|null
332
     */
333
    public function findRedirectMatch(string $fullUrl, string $pathOnly, $siteId = null)
334
    {
335
        // Get the current site
336
        if ($siteId === null) {
0 ignored issues
show
introduced by
The condition $siteId === null is always true.
Loading history...
337
            $currentSite = Craft::$app->getSites()->currentSite;
338
            if ($currentSite) {
339
                $siteId = $currentSite->id;
340
            } else {
341
                $primarySite = Craft::$app->getSites()->primarySite;
342
                if ($currentSite) {
343
                    $siteId = $primarySite->id;
344
                }
345
            }
346
        }
347
        // Try getting the full URL redirect from the cache
348
        $redirect = $this->getRedirectFromCache($fullUrl, $siteId);
349
        if ($redirect) {
350
            $this->incrementRedirectHitCount($redirect);
351
            $this->saveRedirectToCache($fullUrl, $redirect);
352
353
            return $redirect;
354
        }
355
        // Try getting the path only redirect from the cache
356
        $redirect = $this->getRedirectFromCache($pathOnly, $siteId);
357
        if ($redirect) {
358
            $this->incrementRedirectHitCount($redirect);
359
            $this->saveRedirectToCache($pathOnly, $redirect);
360
361
            return $redirect;
362
        }
363
364
        $redirect = $this->getStaticRedirect($fullUrl, $pathOnly, $siteId);
365
        if ($redirect) {
366
            $this->incrementRedirectHitCount($redirect);
367
            $this->saveRedirectToCache($pathOnly, $redirect);
368
369
            return $redirect;
370
        }
371
372
        // Resolve static redirects
373
        $redirects = $this->getAllRegExRedirects(null, $siteId);
374
        $redirect = $this->resolveRedirect($fullUrl, $pathOnly, $redirects, $siteId);
375
        if ($redirect) {
376
            return $redirect;
377
        }
378
379
        return null;
380
    }
381
382
    /**
0 ignored issues
show
Coding Style introduced by
Missing short description in doc comment
Loading history...
383
     * @param          $url
0 ignored issues
show
Coding Style introduced by
Tag value for @param tag indented incorrectly; expected 1 spaces but found 10
Loading history...
Coding Style introduced by
Missing parameter comment
Loading history...
384
     * @param int|null $siteId
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
385
     *
386
     * @return bool|array
387
     */
388
    public function getRedirectFromCache($url, int $siteId = 0)
389
    {
390
        $cache = Craft::$app->getCache();
391
        $cacheKey = $this::CACHE_KEY . md5($url) . $siteId;
392
        $redirect = $cache->get($cacheKey);
393
        Craft::info(
394
            Craft::t(
395
                'retour',
396
                'Cached redirect hit for {url}',
397
                ['url' => $url]
398
            ),
399
            __METHOD__
400
        );
401
402
        return $redirect;
403
    }
404
405
    /**
0 ignored issues
show
Coding Style introduced by
Missing short description in doc comment
Loading history...
406
     * @param string $url
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
407
     * @param array $redirect
0 ignored issues
show
Coding Style introduced by
Expected 2 spaces after parameter type; 1 found
Loading history...
Coding Style introduced by
Missing parameter comment
Loading history...
408
     */
0 ignored issues
show
Coding Style introduced by
Missing @return tag in function comment
Loading history...
409
    public function saveRedirectToCache($url, $redirect)
410
    {
411
        $cache = Craft::$app->getCache();
412
        // Get the current site id
413
        $sites = Craft::$app->getSites();
414
        try {
415
            $siteId = $sites->getCurrentSite()->id;
416
        } catch (SiteNotFoundException $e) {
417
            $siteId = 1;
418
        }
419
        $cacheKey = $this::CACHE_KEY . md5($url) . $siteId;
420
        // Create the dependency tags
421
        $dependency = new TagDependency([
0 ignored issues
show
Coding Style introduced by
The opening parenthesis of a multi-line function call should be the last content on the line.
Loading history...
422
            'tags' => [
423
                $this::GLOBAL_REDIRECTS_CACHE_TAG,
424
                $this::GLOBAL_REDIRECTS_CACHE_TAG . $siteId,
425
            ],
426
        ]);
0 ignored issues
show
Coding Style introduced by
For multi-line function calls, the closing parenthesis should be on a new line.

If a function call spawns multiple lines, the coding standard suggests to move the closing parenthesis to a new line:

someFunctionCall(
    $firstArgument,
    $secondArgument,
    $thirdArgument
); // Closing parenthesis on a new line.
Loading history...
427
        $cache->set($cacheKey, $redirect, Retour::$cacheDuration, $dependency);
428
        Craft::info(
429
            Craft::t(
430
                'retour',
431
                'Cached redirect saved for {url}',
432
                ['url' => $url]
433
            ),
434
            __METHOD__
435
        );
436
    }
437
438
    /**
0 ignored issues
show
Coding Style introduced by
Missing short description in doc comment
Loading history...
Coding Style introduced by
Parameter $siteId should have a doc-comment as per coding-style.
Loading history...
439
     * @param string $fullUrl
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
440
     * @param string $pathOnly
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
441
     * @param array $redirects
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
Coding Style introduced by
Expected 2 spaces after parameter type; 1 found
Loading history...
442
     *
443
     * @return array|null
444
     */
445
    public function resolveRedirect(string $fullUrl, string $pathOnly, array $redirects, $siteId)
446
    {
447
        $result = null;
448
        // Throw the Redirects::EVENT_BEFORE_RESOLVE_REDIRECT event
449
        $event = new ResolveRedirectEvent([
0 ignored issues
show
Coding Style introduced by
The opening parenthesis of a multi-line function call should be the last content on the line.
Loading history...
450
            'fullUrl' => $fullUrl,
451
            'pathOnly' => $pathOnly,
452
            'redirectDestUrl' => null,
453
            'redirectHttpCode' => 301,
454
            'siteId' => $siteId,
455
        ]);
0 ignored issues
show
Coding Style introduced by
For multi-line function calls, the closing parenthesis should be on a new line.

If a function call spawns multiple lines, the coding standard suggests to move the closing parenthesis to a new line:

someFunctionCall(
    $firstArgument,
    $secondArgument,
    $thirdArgument
); // Closing parenthesis on a new line.
Loading history...
456
        $this->trigger(self::EVENT_BEFORE_RESOLVE_REDIRECT, $event);
457
        if ($event->redirectDestUrl !== null) {
458
            return $this->resolveEventRedirect($event);
459
        }
460
        // Iterate through the redirects
461
        foreach ($redirects as $redirect) {
462
            // Figure out what type of source matching to do
463
            $redirectSrcMatch = $redirect['redirectSrcMatch'] ?? 'pathonly';
464
            $redirectEnabled = (bool)$redirect['enabled'];
465
            if ($redirectEnabled === true) {
466
                switch ($redirectSrcMatch) {
467
                    case 'pathonly':
0 ignored issues
show
Coding Style introduced by
Line indented incorrectly; expected 16 spaces, found 20
Loading history...
468
                        $url = $pathOnly;
469
                        break;
470
                    case 'fullurl':
0 ignored issues
show
Coding Style introduced by
Line indented incorrectly; expected 16 spaces, found 20
Loading history...
471
                        $url = $fullUrl;
472
                        break;
473
                    default:
0 ignored issues
show
Coding Style introduced by
Line indented incorrectly; expected 16 spaces, found 20
Loading history...
474
                        $url = $pathOnly;
475
                        break;
476
                }
477
                $redirectMatchType = $redirect['redirectMatchType'] ?? 'notfound';
478
                switch ($redirectMatchType) {
479
                    // Do a straight up match
480
                    case 'exactmatch':
0 ignored issues
show
Coding Style introduced by
Line indented incorrectly; expected 16 spaces, found 20
Loading history...
481
                        if (strcasecmp($redirect['redirectSrcUrlParsed'], $url) === 0) {
0 ignored issues
show
Coding Style introduced by
Line indented incorrectly; expected 20 spaces, found 24
Loading history...
482
                            $this->incrementRedirectHitCount($redirect);
483
                            $this->saveRedirectToCache($url, $redirect);
484
485
                            // Throw the Redirects::EVENT_REDIRECT_RESOLVED event
486
                            $event = new RedirectResolvedEvent([
0 ignored issues
show
Coding Style introduced by
The opening parenthesis of a multi-line function call should be the last content on the line.
Loading history...
487
                                'fullUrl' => $fullUrl,
488
                                'pathOnly' => $pathOnly,
489
                                'redirectDestUrl' => null,
490
                                'redirectHttpCode' => 301,
491
                                'redirect' => $redirect,
492
                                'siteId' => $siteId,
493
                            ]);
0 ignored issues
show
Coding Style introduced by
For multi-line function calls, the closing parenthesis should be on a new line.

If a function call spawns multiple lines, the coding standard suggests to move the closing parenthesis to a new line:

someFunctionCall(
    $firstArgument,
    $secondArgument,
    $thirdArgument
); // Closing parenthesis on a new line.
Loading history...
494
                            $this->trigger(self::EVENT_REDIRECT_RESOLVED, $event);
495
                            if ($event->redirectDestUrl !== null) {
0 ignored issues
show
Coding Style introduced by
Line indented incorrectly; expected 20 spaces, found 28
Loading history...
496
                                return $this->resolveEventRedirect($event, $url, $redirect);
497
                            }
0 ignored issues
show
Coding Style introduced by
Line indented incorrectly; expected 20 spaces, found 28
Loading history...
498
499
                            return $redirect;
500
                        }
0 ignored issues
show
Coding Style introduced by
Line indented incorrectly; expected 20 spaces, found 24
Loading history...
501
                        break;
502
503
                    // Do a regex match
504
                    case 'regexmatch':
0 ignored issues
show
Coding Style introduced by
Line indented incorrectly; expected 16 spaces, found 20
Loading history...
505
                        $matchRegEx = '`' . $redirect['redirectSrcUrlParsed'] . '`i';
506
                        try {
0 ignored issues
show
Coding Style introduced by
Line indented incorrectly; expected 20 spaces, found 24
Loading history...
507
                            if (preg_match($matchRegEx, $url) === 1) {
0 ignored issues
show
Coding Style introduced by
Line indented incorrectly; expected 24 spaces, found 28
Loading history...
508
                                $this->incrementRedirectHitCount($redirect);
509
                                // If we're not associated with an EntryID, handle capture group replacement
510
                                if ((int)$redirect['associatedElementId'] === 0) {
0 ignored issues
show
Coding Style introduced by
Line indented incorrectly; expected 28 spaces, found 32
Loading history...
511
                                    $redirect['redirectDestUrl'] = preg_replace(
512
                                        $matchRegEx,
513
                                        $redirect['redirectDestUrl'],
514
                                        $url
515
                                    );
516
                                }
0 ignored issues
show
Coding Style introduced by
Line indented incorrectly; expected 28 spaces, found 32
Loading history...
517
                                $url = preg_replace('/([^:])(\/{2,})/', '$1/', $url);
518
                                $this->saveRedirectToCache($url, $redirect);
519
520
                                // Throw the Redirects::EVENT_REDIRECT_RESOLVED event
521
                                $event = new RedirectResolvedEvent([
0 ignored issues
show
Coding Style introduced by
The opening parenthesis of a multi-line function call should be the last content on the line.
Loading history...
522
                                    'fullUrl' => $fullUrl,
523
                                    'pathOnly' => $pathOnly,
524
                                    'redirectDestUrl' => null,
525
                                    'redirectHttpCode' => 301,
526
                                    'redirect' => $redirect,
527
                                    'siteId' => $siteId,
528
                                ]);
0 ignored issues
show
Coding Style introduced by
For multi-line function calls, the closing parenthesis should be on a new line.

If a function call spawns multiple lines, the coding standard suggests to move the closing parenthesis to a new line:

someFunctionCall(
    $firstArgument,
    $secondArgument,
    $thirdArgument
); // Closing parenthesis on a new line.
Loading history...
529
                                $this->trigger(self::EVENT_REDIRECT_RESOLVED, $event);
530
                                if ($event->redirectDestUrl !== null) {
0 ignored issues
show
Coding Style introduced by
Line indented incorrectly; expected 24 spaces, found 32
Loading history...
531
                                    return $this->resolveEventRedirect($event, $url, $redirect);
532
                                }
0 ignored issues
show
Coding Style introduced by
Line indented incorrectly; expected 24 spaces, found 32
Loading history...
533
534
                                return $redirect;
535
                            }
0 ignored issues
show
Coding Style introduced by
Line indented incorrectly; expected 24 spaces, found 28
Loading history...
536
                        } catch (\Exception $e) {
0 ignored issues
show
Coding Style introduced by
Line indented incorrectly; expected 20 spaces, found 24
Loading history...
537
                            // That's fine
538
                            Craft::error('Invalid Redirect Regex: ' . $matchRegEx, __METHOD__);
539
                        }
0 ignored issues
show
Coding Style introduced by
Line indented incorrectly; expected 20 spaces, found 24
Loading history...
540
541
                        break;
542
543
                    // Otherwise try to look up a plugin's method by and call it for the match
544
                    default:
0 ignored issues
show
Coding Style introduced by
Line indented incorrectly; expected 16 spaces, found 20
Loading history...
545
                        $plugin = $redirectMatchType ? Craft::$app->getPlugins()->getPlugin($redirectMatchType) : null;
546
                        if ($plugin && method_exists($plugin, 'retourMatch')) {
0 ignored issues
show
Coding Style introduced by
Line indented incorrectly; expected 20 spaces, found 24
Loading history...
547
                            $args = [
548
                                [
549
                                    'redirect' => &$redirect,
550
                                ],
551
                            ];
552
                            $result = \call_user_func_array([$plugin, 'retourMatch'], $args);
553
                            if ($result) {
0 ignored issues
show
Coding Style introduced by
Line indented incorrectly; expected 24 spaces, found 28
Loading history...
554
                                $this->incrementRedirectHitCount($redirect);
555
                                $this->saveRedirectToCache($url, $redirect);
556
557
                                // Throw the Redirects::EVENT_REDIRECT_RESOLVED event
558
                                $event = new RedirectResolvedEvent([
0 ignored issues
show
Coding Style introduced by
The opening parenthesis of a multi-line function call should be the last content on the line.
Loading history...
559
                                    'fullUrl' => $fullUrl,
560
                                    'pathOnly' => $pathOnly,
561
                                    'redirectDestUrl' => null,
562
                                    'redirectHttpCode' => 301,
563
                                    'redirect' => $redirect,
564
                                    'siteId' => $siteId,
565
                                ]);
0 ignored issues
show
Coding Style introduced by
For multi-line function calls, the closing parenthesis should be on a new line.

If a function call spawns multiple lines, the coding standard suggests to move the closing parenthesis to a new line:

someFunctionCall(
    $firstArgument,
    $secondArgument,
    $thirdArgument
); // Closing parenthesis on a new line.
Loading history...
566
                                $this->trigger(self::EVENT_REDIRECT_RESOLVED, $event);
567
                                if ($event->redirectDestUrl !== null) {
0 ignored issues
show
Coding Style introduced by
Line indented incorrectly; expected 24 spaces, found 32
Loading history...
568
                                    return $this->resolveEventRedirect($event, $url, $redirect);
569
                                }
0 ignored issues
show
Coding Style introduced by
Line indented incorrectly; expected 24 spaces, found 32
Loading history...
570
571
                                return $redirect;
572
                            }
0 ignored issues
show
Coding Style introduced by
Line indented incorrectly; expected 24 spaces, found 28
Loading history...
573
                        }
0 ignored issues
show
Coding Style introduced by
Line indented incorrectly; expected 20 spaces, found 24
Loading history...
574
                        break;
575
                }
576
            }
577
        }
578
        // Throw the Redirects::EVENT_AFTER_RESOLVE_REDIRECT event
579
        $event = new ResolveRedirectEvent([
0 ignored issues
show
Coding Style introduced by
The opening parenthesis of a multi-line function call should be the last content on the line.
Loading history...
580
            'fullUrl' => $fullUrl,
581
            'pathOnly' => $pathOnly,
582
            'redirectDestUrl' => null,
583
            'redirectHttpCode' => 301,
584
            'siteId' => $siteId,
585
        ]);
0 ignored issues
show
Coding Style introduced by
For multi-line function calls, the closing parenthesis should be on a new line.

If a function call spawns multiple lines, the coding standard suggests to move the closing parenthesis to a new line:

someFunctionCall(
    $firstArgument,
    $secondArgument,
    $thirdArgument
); // Closing parenthesis on a new line.
Loading history...
586
        $this->trigger(self::EVENT_AFTER_RESOLVE_REDIRECT, $event);
587
        if ($event->redirectDestUrl !== null) {
588
            return $this->resolveEventRedirect($event);
589
        }
590
        Craft::info(
591
            Craft::t(
592
                'retour',
593
                'Not handled-> full URL: {fullUrl}, path only: {pathOnly}',
594
                ['fullUrl' => $fullUrl, 'pathOnly' => $pathOnly]
595
            ),
596
            __METHOD__
597
        );
598
599
        return $result;
600
    }
601
602
    /**
0 ignored issues
show
Coding Style introduced by
Missing short description in doc comment
Loading history...
Coding Style introduced by
Parameter $url should have a doc-comment as per coding-style.
Loading history...
Coding Style introduced by
Parameter $redirect should have a doc-comment as per coding-style.
Loading history...
603
     * @param ResolveRedirectEvent $event
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
604
     *
605
     * @return null|array
606
     */
607
    public function resolveEventRedirect(ResolveRedirectEvent $event, $url = null, $redirect = null)
608
    {
609
        $result = null;
610
611
        if ($event->redirectDestUrl !== null) {
612
            $resolvedRedirect = new StaticRedirectsModel([
0 ignored issues
show
Coding Style introduced by
The opening parenthesis of a multi-line function call should be the last content on the line.
Loading history...
613
                'id' => self::EVENT_REDIRECT_ID,
614
                'redirectDestUrl' => $event->redirectDestUrl,
615
                'redirectHttpCode' => $event->redirectHttpCode,
616
            ]);
0 ignored issues
show
Coding Style introduced by
For multi-line function calls, the closing parenthesis should be on a new line.

If a function call spawns multiple lines, the coding standard suggests to move the closing parenthesis to a new line:

someFunctionCall(
    $firstArgument,
    $secondArgument,
    $thirdArgument
); // Closing parenthesis on a new line.
Loading history...
617
            $result = $resolvedRedirect->toArray();
618
619
            if ($url !== null && $redirect !== null) {
620
                // Save the modified redirect to the cache
621
                $redirect['redirectDestUrl'] = $event->redirectDestUrl;
622
                $redirect['redirectHttpCode'] = $event->redirectHttpCode;
623
                $this->saveRedirectToCache($url, $redirect);
624
            }
625
        }
626
627
        return $result;
628
    }
629
630
    /**
631
     * Returns the list of matching schemes
632
     *
633
     * @return  array
0 ignored issues
show
Coding Style introduced by
Tag value for @return tag indented incorrectly; expected 1 spaces but found 2
Loading history...
634
     */
635
    public function getMatchesList(): array
636
    {
637
        $result = [
638
            'exactmatch' => Craft::t('retour', 'Exact Match'),
639
            'regexmatch' => Craft::t('retour', 'RegEx Match'),
640
        ];
641
642
        // Add any plugins that offer the retourMatch() method
643
        foreach (Craft::$app->getPlugins()->getAllPlugins() as $plugin) {
644
            /** @var Plugin $plugin */
0 ignored issues
show
Coding Style introduced by
The open comment tag must be the only content on the line
Loading history...
Coding Style introduced by
Missing short description in doc comment
Loading history...
Coding Style introduced by
The close comment tag must be the only content on the line
Loading history...
645
            if (method_exists($plugin, 'retourMatch')) {
646
                $result[$plugin->getHandle()] = $plugin->name . Craft::t('retour', ' Match');
647
            }
648
        }
649
650
        return $result;
651
    }
652
653
    /**
0 ignored issues
show
Coding Style introduced by
Missing short description in doc comment
Loading history...
654
     * @param string $fullUrl
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
Coding Style introduced by
Tag value for @param tag indented incorrectly; expected 2 spaces but found 1
Loading history...
655
     * @param string $pathOnly
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
Coding Style introduced by
Tag value for @param tag indented incorrectly; expected 2 spaces but found 1
Loading history...
656
     * @param $siteId
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
Coding Style introduced by
Tag value for @param tag indented incorrectly; expected 2 spaces but found 1
Loading history...
657
     * @return mixed|null
0 ignored issues
show
Coding Style introduced by
Tag @return cannot be grouped with parameter tags in a doc comment
Loading history...
658
     */
659
    public function getStaticRedirect(string $fullUrl, string $pathOnly, $siteId)
660
    {
661
        $staticCondition = ['redirectMatchType' => 'exactmatch'];
662
        $siteCondition = [
663
            'or',
664
            ['siteId' => $siteId],
665
            ['siteId' => null]
666
        ];
667
        $pathCondition = [
668
            'or',
669
            ['and',
670
                ['redirectSrcMatch' => 'pathonly'],
671
                ['redirectSrcUrlParsed' => $pathOnly]
672
            ],
673
            ['and',
674
                ['redirectSrcMatch' => 'fullurl'],
675
                ['redirectSrcUrlParsed' => $fullUrl]
676
            ],
677
        ];
678
679
        $query = (new Query)
680
            ->from('{{%retour_static_redirects}}')
681
            ->where(['and',
0 ignored issues
show
Coding Style introduced by
The opening parenthesis of a multi-line function call should be the last content on the line.
Loading history...
682
                $staticCondition,
683
                $pathCondition,
684
                $siteCondition
685
            ])
0 ignored issues
show
Coding Style introduced by
For multi-line function calls, the closing parenthesis should be on a new line.

If a function call spawns multiple lines, the coding standard suggests to move the closing parenthesis to a new line:

someFunctionCall(
    $firstArgument,
    $secondArgument,
    $thirdArgument
); // Closing parenthesis on a new line.
Loading history...
686
            ->limit(1);
687
688
        return $query->one();
689
    }
690
691
    /**
0 ignored issues
show
Coding Style introduced by
Missing short description in doc comment
Loading history...
692
     * @param null|int $limit
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
693
     * @param int|null $siteId
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
694
     *
695
     * @return array All of the regex match redirects
696
     */
697
    public function getAllRegExRedirects(int $limit = null, int $siteId = null): array
698
    {
699
        // Cache it in our class; no need to fetch it more than once
700
        if ($this->cachedRegExRedirects !== null) {
701
            return $this->cachedRegExRedirects;
702
        }
703
704
        $redirects = $this->getRedirectsByMatchType($limit, $siteId, 'regexmatch');
705
706
        // Cache for future accesses
707
        $this->cachedRegExRedirects = $redirects;
708
709
        return $redirects;
710
    }
711
712
    /**
0 ignored issues
show
Coding Style introduced by
Missing short description in doc comment
Loading history...
713
     * @param null|int $limit
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
714
     * @param int|null $siteId
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
715
     *
716
     * @return array All of the regex match redirects
717
     */
718
    public function getAllExactMatchRedirects(int $limit = null, int $siteId = null): array
719
    {
720
        // Cache it in our class; no need to fetch it more than once
721
        if ($this->cachedExactMatchRedirects !== null) {
722
            return $this->cachedExactMatchRedirects;
723
        }
724
725
        $redirects = $this->getRedirectsByMatchType($limit, $siteId, 'exactmatch');
726
727
        // Cache for future accesses
728
        $this->cachedExactMatchRedirects = $redirects;
729
730
        return $redirects;
731
    }
732
733
    /**
0 ignored issues
show
Coding Style introduced by
Missing short description in doc comment
Loading history...
734
     * @param int|null $limit
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
Coding Style introduced by
Tag value for @param tag indented incorrectly; expected 2 spaces but found 1
Loading history...
735
     * @param int|null $siteId
0 ignored issues
show
Coding Style introduced by
Tag value for @param tag indented incorrectly; expected 2 spaces but found 1
Loading history...
Coding Style introduced by
Missing parameter comment
Loading history...
736
     * @param string $type
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
Coding Style introduced by
Expected 3 spaces after parameter type; 1 found
Loading history...
Coding Style introduced by
Tag value for @param tag indented incorrectly; expected 2 spaces but found 1
Loading history...
737
     * @return array
0 ignored issues
show
Coding Style introduced by
Tag @return cannot be grouped with parameter tags in a doc comment
Loading history...
738
     */
739
    protected function getRedirectsByMatchType(int $limit = null, int $siteId = null, string $type): array
740
    {
741
        // Query the db table
742
        $query = (new Query())
743
            ->from(['{{%retour_static_redirects}}'])
744
            ->orderBy('redirectMatchType ASC, redirectSrcMatch ASC, hitCount DESC');
745
746
        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...
747
            $query
748
                ->where(['siteId' => $siteId])
749
                ->orWhere(['siteId' => null]);
750
        }
751
752
        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...
753
            $query->limit($limit);
754
        }
755
756
        $query->andWhere(['redirectMatchType' => $type]);
757
758
        return $query->all();
759
    }
760
761
    /**
0 ignored issues
show
Coding Style introduced by
Missing short description in doc comment
Loading history...
762
     * @param null|int $limit
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
763
     * @param int|null $siteId
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
764
     *
765
     * @return array All of the statistics
766
     */
767
    public function getAllStaticRedirects($limit = null, int $siteId = null): array
768
    {
769
        // Cache it in our class; no need to fetch it more than once
770
        if ($this->cachedStaticRedirects !== null) {
771
            return $this->cachedStaticRedirects;
772
        }
773
        // Query the db table
774
        $query = (new Query())
775
            ->from(['{{%retour_static_redirects}}'])
776
            ->orderBy('redirectMatchType ASC, redirectSrcMatch ASC, hitCount DESC');
777
        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...
778
            $query
779
                ->where(['siteId' => $siteId])
780
                ->orWhere(['siteId' => null]);
781
        }
782
        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...
783
            $query->limit($limit);
784
        }
785
        $redirects = $query->all();
786
        // Cache for future accesses
787
        $this->cachedStaticRedirects = $redirects;
788
789
        return $redirects;
790
    }
791
792
    /**
793
     * Return a redirect by id
794
     *
795
     * @param int $id
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
796
     *
797
     * @return null|array The static redirect
798
     */
799
    public function getRedirectById(int $id)
800
    {
801
        // Query the db table
802
        $redirect = (new Query())
803
            ->from(['{{%retour_static_redirects}}'])
804
            ->where(['id' => $id])
805
            ->one();
806
807
        return $redirect;
808
    }
809
810
    /**
811
     * Return a redirect by redirectSrcUrl
812
     *
813
     * @param string $redirectSrcUrl
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
Coding Style introduced by
Expected 3 spaces after parameter type; 1 found
Loading history...
814
     * @param int|null $siteId
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
815
     *
816
     * @return null|array
817
     */
818
    public function getRedirectByRedirectSrcUrl(string $redirectSrcUrl, int $siteId = null)
819
    {
820
        // Query the db table
821
        $query = (new Query())
822
            ->from(['{{%retour_static_redirects}}'])
823
            ->where(['redirectSrcUrl' => $redirectSrcUrl]);
824
        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...
825
            $query
826
                ->andWhere(['or', [
0 ignored issues
show
Coding Style introduced by
The opening parenthesis of a multi-line function call should be the last content on the line.
Loading history...
827
                    'siteId' => $siteId,
828
                ], [
0 ignored issues
show
Coding Style introduced by
This line of the multi-line function call does not seem to be indented correctly. Expected 20 spaces, but found 16.
Loading history...
829
                    'siteId' => null,
830
                ]]);
0 ignored issues
show
Coding Style introduced by
For multi-line function calls, the closing parenthesis should be on a new line.

If a function call spawns multiple lines, the coding standard suggests to move the closing parenthesis to a new line:

someFunctionCall(
    $firstArgument,
    $secondArgument,
    $thirdArgument
); // Closing parenthesis on a new line.
Loading history...
831
        }
832
        $redirect = $query->one();
833
834
        return $redirect;
835
    }
836
837
    /**
838
     * Return redirects for a given element.
839
     *
840
     * @param int $elementId
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
Coding Style introduced by
Expected 6 spaces after parameter type; 1 found
Loading history...
841
     * @param int|null $siteId
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
842
     *
843
     * @return null|array
844
     */
845
    public function getRedirectsByElementId(int $elementId, int $siteId = null)
846
    {
847
        // Query the db table
848
        $query = (new Query())
849
            ->from(['{{%retour_static_redirects}}'])
850
            ->where(['associatedElementId' => $elementId]);
851
        if ($siteId !== null) {
852
            $query->andWhere(['siteId' => $siteId]);
853
        }
854
855
        return $query->all();
856
    }
857
858
    /**
859
     * Delete a redirect by id
860
     *
861
     * @param int $id
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
862
     *
863
     * @return int The result
864
     */
865
    public function deleteRedirectById(int $id): int
866
    {
867
        $db = Craft::$app->getDb();
868
        // Delete a row from the db table
869
        try {
870
            $result = $db->createCommand()->delete(
871
                '{{%retour_static_redirects}}',
872
                [
873
                    'id' => $id,
874
                ]
875
            )->execute();
876
        } catch (Exception $e) {
877
            Craft::error($e->getMessage(), __METHOD__);
878
            $result = 0;
879
        }
880
881
        return $result;
882
    }
883
884
    /**
885
     * Increment the retour_static_redirects record
886
     *
887
     * @param $redirectConfig
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
888
     */
0 ignored issues
show
Coding Style introduced by
Missing @return tag in function comment
Loading history...
889
    public function incrementRedirectHitCount(&$redirectConfig)
890
    {
891
        if ($redirectConfig !== null) {
892
            $db = Craft::$app->getDb();
893
            $redirectConfig['hitCount']++;
894
            $redirectConfig['hitLastTime'] = Db::prepareDateForDb(new \DateTime());
895
            Craft::debug(
896
                Craft::t(
897
                    'retour',
898
                    'Incrementing statistics for: {redirect}',
899
                    ['redirect' => print_r($redirectConfig, true)]
900
                ),
901
                __METHOD__
902
            );
903
            // Update the existing record
904
            try {
905
                $rowsAffected = $db->createCommand()->update(
906
                    '{{%retour_static_redirects}}',
907
                    [
908
                        'hitCount' => $redirectConfig['hitCount'],
909
                        'hitLastTime' => $redirectConfig['hitLastTime'],
910
                    ],
911
                    [
912
                        'id' => $redirectConfig['id'],
913
                    ]
914
                )->execute();
915
                Craft::debug('Rows affected: ' . $rowsAffected, __METHOD__);
916
            } catch (\Exception $e) {
917
                Craft::error($e->getMessage(), __METHOD__);
918
            }
919
        }
920
    }
921
922
    /**
923
     * Save an element redirect.
924
     *
925
     * @param ElementInterface $element
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
926
     * @param string $sourceUrl
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
Coding Style introduced by
Expected 11 spaces after parameter type; 1 found
Loading history...
927
     * @param string $redirectSrcMatch
0 ignored issues
show
Coding Style introduced by
Expected 11 spaces after parameter type; 1 found
Loading history...
Coding Style introduced by
Missing parameter comment
Loading history...
928
     * @param int $redirectHttpCode
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
Coding Style introduced by
Expected 14 spaces after parameter type; 1 found
Loading history...
929
     */
0 ignored issues
show
Coding Style introduced by
Missing @return tag in function comment
Loading history...
930
    public function enableElementRedirect(ElementInterface $element, string $sourceUrl, string $redirectSrcMatch = 'pathonly', int $redirectHttpCode = 301)
931
    {
932
        $siteId = $redirectSrcMatch === 'pathonly' ? null : $element->siteId;
0 ignored issues
show
Bug introduced by
Accessing siteId on the interface craft\base\ElementInterface suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
933
        $parentElement = ElementHelper::rootElement($element);
934
935
        $redirectConfig = [
936
            'redirectMatchType' => 'exactmatch',
937
            'redirectSrcUrl' => $sourceUrl,
938
            'siteId' => $siteId,
939
            'associatedElementId' => $element->getCanonicalId(),
940
            'enabled' => $parentElement->getEnabledForSite($siteId),
941
            'redirectSrcMatch' => $redirectSrcMatch,
942
            'redirectDestUrl' => $redirectSrcMatch === 'pathonly' ? $parentElement->uri : $parentElement->getUrl(),
0 ignored issues
show
Bug introduced by
Accessing uri on the interface craft\base\ElementInterface suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
943
            'redirectHttpCode' => $redirectHttpCode,
944
        ];
945
946
        $this->saveRedirect($redirectConfig);
947
    }
948
949
    /**
0 ignored issues
show
Coding Style introduced by
Parameter $allSites should have a doc-comment as per coding-style.
Loading history...
950
     * Delete an element redirect.
951
     *
952
     * @param ElementInterface $element
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
953
     */
0 ignored issues
show
Coding Style introduced by
Missing @return tag in function comment
Loading history...
954
    public function removeElementRedirect(ElementInterface $element, bool $allSites = false)
955
    {
956
        $redirects = $this->getRedirectsByElementId($element->getCanonicalId(), $allSites ? null : $element->siteId);
0 ignored issues
show
Bug introduced by
Accessing siteId on the interface craft\base\ElementInterface suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
957
958
        if (!empty($redirects)) {
959
            foreach ($redirects as $redirect) {
960
                $this->deleteRedirectById($redirect['id']);
961
            }
962
        }
963
    }
964
965
    /**
966
     * Delete a short link by its ID.
967
     *
968
     * @param int $redirectId
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
Coding Style introduced by
Tag value for @param tag indented incorrectly; expected 2 spaces but found 1
Loading history...
969
     * @throws \Throwable
0 ignored issues
show
Coding Style introduced by
Tag @throws cannot be grouped with parameter tags in a doc comment
Loading history...
970
     * @throws \craft\errors\ElementNotFoundException
0 ignored issues
show
Coding Style introduced by
Tag @throws cannot be grouped with parameter tags in a doc comment
Loading history...
971
     * @throws \yii\base\Exception
0 ignored issues
show
Coding Style introduced by
Tag @throws cannot be grouped with parameter tags in a doc comment
Loading history...
972
     */
0 ignored issues
show
Coding Style introduced by
Missing @return tag in function comment
Loading history...
973
    public function deleteShortlinkById(int $redirectId)
974
    {
975
        $redirect = $this->getRedirectById($redirectId);
976
        $elementId = $redirect['associatedElementId'];
977
        $siteId = $redirect['siteId'];
978
        $element = Craft::$app->getElements()->getElementById($elementId, null, $siteId);
979
980
        if ($element) {
981
            $layout = $element->getFieldLayout();
982
            $srcUrl = $redirect['redirectSrcUrl'];
983
            $site = $element->getSite();
984
            $urlLess = StringHelper::removeLeft($srcUrl, $site->getBaseUrl());
985
986
            $match = false;
987
            foreach ($element->getFieldValues() as $fieldHandle => $fieldValue) {
988
                if (!is_string($fieldValue)) {
989
                    continue;
990
                }
991
                // Compare field value with starting slashes dropped to the redirectSrcUrl value as well as one with site URL removed, just in case
992
                if (in_array(ltrim($fieldValue, '/'), [ltrim($srcUrl, '/'), ltrim($urlLess, '/')], true)) {
993
                    $field = $layout->getFieldByHandle($fieldHandle);
994
995
                    if ($field instanceof ShortLink) {
996
                        $element->setFieldValue($fieldHandle, null);
997
                        $match = true;
998
                    }
999
                }
1000
            }
1001
1002
            if ($match) {
1003
                // This will also delete the redirect from the table
1004
                Craft::$app->getElements()->saveElement($element);
1005
            }
1006
        }
1007
    }
1008
1009
    /**
0 ignored issues
show
Coding Style introduced by
Missing short description in doc comment
Loading history...
1010
     * @param array $redirectConfig
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
Coding Style introduced by
Tag value for @param tag indented incorrectly; expected 2 spaces but found 1
Loading history...
1011
     * @return bool whether the redirect was saved or not
0 ignored issues
show
Coding Style introduced by
Tag @return cannot be grouped with parameter tags in a doc comment
Loading history...
1012
     */
1013
    public function saveRedirect(array $redirectConfig): bool
1014
    {
1015
        // Handle URL encoded URLs by decoding them before saving them
1016
        if (isset($redirectConfig['redirectMatchType']) && $redirectConfig['redirectMatchType'] === 'exactmatch') {
1017
            $redirectConfig['redirectSrcUrl'] = urldecode($redirectConfig['redirectSrcUrl'] ?? '');
1018
            $redirectConfig['redirectSrcUrlParsed'] = urldecode($redirectConfig['redirectSrcUrlParsed'] ?? '');
1019
        }
1020
        // Validate the model before saving it to the db
1021
        $redirect = new StaticRedirectsModel($redirectConfig);
1022
        if ($redirect->validate() === false) {
1023
            Craft::error(
1024
                Craft::t(
1025
                    'retour',
1026
                    'Error validating redirect {id}: {errors}',
1027
                    ['id' => $redirect->id, 'errors' => print_r($redirect->getErrors(), true)]
1028
                ),
1029
                __METHOD__
1030
            );
1031
1032
            return false;
1033
        }
1034
        // Get the validated model attributes and save them to the db
1035
        $redirectConfig = $redirect->getAttributes();
1036
        // 0 for a siteId needs to be converted to null
1037
        if (empty($redirectConfig['siteId']) || (int)$redirectConfig['siteId'] === 0) {
1038
            $redirectConfig['siteId'] = null;
1039
        }
1040
        // Throw an event to before saving the redirect
1041
        $db = Craft::$app->getDb();
1042
        // See if a redirect exists with this source URL already
1043
        if ((int)$redirectConfig['id'] === 0) {
1044
            // Query the db table
1045
            $redirect = (new Query())
1046
                ->from(['{{%retour_static_redirects}}'])
1047
                ->where(['redirectSrcUrlParsed' => $redirectConfig['redirectSrcUrlParsed']])
1048
                ->andWhere(['siteId' => $redirectConfig['siteId']])
1049
                ->one();
1050
            // If it exists, update it rather than having duplicates
1051
            if (!empty($redirect)) {
1052
                $redirectConfig['id'] = $redirect['id'];
1053
            }
1054
        }
1055
        // Trigger a 'beforeSaveRedirect' event
1056
        $isNew = (int)$redirectConfig['id'] === 0;
1057
        $event = new RedirectEvent([
0 ignored issues
show
Coding Style introduced by
The opening parenthesis of a multi-line function call should be the last content on the line.
Loading history...
1058
            'isNew' => $isNew,
1059
            'legacyUrl' => $redirectConfig['redirectSrcUrlParsed'],
1060
            'destinationUrl' => $redirectConfig['redirectDestUrl'],
1061
            'matchType' => $redirectConfig['redirectSrcMatch'],
1062
            'redirectType' => $redirectConfig['redirectHttpCode'],
1063
            'siteId' => $redirectConfig['siteId'],
1064
        ]);
0 ignored issues
show
Coding Style introduced by
For multi-line function calls, the closing parenthesis should be on a new line.

If a function call spawns multiple lines, the coding standard suggests to move the closing parenthesis to a new line:

someFunctionCall(
    $firstArgument,
    $secondArgument,
    $thirdArgument
); // Closing parenthesis on a new line.
Loading history...
1065
        $this->trigger(self::EVENT_BEFORE_SAVE_REDIRECT, $event);
1066
        if (!$event->isValid) {
1067
            return false;
1068
        }
1069
        // See if this is an existing redirect
1070
        if (!$isNew) {
1071
            Craft::debug(
1072
                Craft::t(
1073
                    'retour',
1074
                    'Updating existing redirect: {redirect}',
1075
                    ['redirect' => print_r($redirectConfig, true)]
1076
                ),
1077
                __METHOD__
1078
            );
1079
            // Update the existing record
1080
            try {
1081
                $db->createCommand()->update(
1082
                    '{{%retour_static_redirects}}',
1083
                    $redirectConfig,
1084
                    [
1085
                        'id' => $redirectConfig['id'],
1086
                    ]
1087
                )->execute();
1088
            } catch (Exception $e) {
1089
                Craft::error($e->getMessage(), __METHOD__);
1090
1091
                return false;
1092
            }
1093
        } else {
1094
            Craft::debug(
1095
                Craft::t(
1096
                    'retour',
1097
                    'Creating new redirect: {redirect}',
1098
                    ['redirect' => print_r($redirectConfig, true)]
1099
                ),
1100
                __METHOD__
1101
            );
1102
            unset($redirectConfig['id']);
1103
            // Create a new record
1104
            try {
1105
                $db->createCommand()->insert(
1106
                    '{{%retour_static_redirects}}',
1107
                    $redirectConfig
1108
                )->execute();
1109
            } catch (Exception $e) {
1110
                Craft::error($e->getMessage(), __METHOD__);
1111
1112
                return false;
1113
            }
1114
        }
1115
        // To prevent redirect loops, see if any static redirects have our redirectDestUrl as their redirectSrcUrl
1116
        $testRedirectConfig = $this->getRedirectByRedirectSrcUrl(
1117
            $redirectConfig['redirectDestUrl'],
1118
            $redirectConfig['siteId']
1119
        );
1120
        if ($testRedirectConfig !== null) {
1121
            Craft::debug(
1122
                Craft::t(
1123
                    'retour',
1124
                    'Deleting redirect to prevent a loop: {redirect}',
1125
                    ['redirect' => print_r($testRedirectConfig, true)]
1126
                ),
1127
                __METHOD__
1128
            );
1129
            // Delete the redirect that has a redirectSrcUrl the same as this record's redirectDestUrl
1130
            try {
1131
                $db->createCommand()->delete(
1132
                    '{{%retour_static_redirects}}',
1133
                    ['id' => $testRedirectConfig['id']]
1134
                )->execute();
1135
            } catch (Exception $e) {
1136
                Craft::error($e->getMessage(), __METHOD__);
1137
            }
1138
        }
1139
        // Trigger a 'afterSaveRedirect' event
1140
        $this->trigger(self::EVENT_AFTER_SAVE_REDIRECT, $event);
1141
1142
        // Invalidate caches after saving a redirect
1143
        $this->invalidateCaches();
1144
1145
        return true;
1146
    }
1147
1148
    /**
1149
     * Invalidate all of the redirects caches
1150
     */
0 ignored issues
show
Coding Style introduced by
Missing @return tag in function comment
Loading history...
1151
    public function invalidateCaches()
1152
    {
1153
        $cache = Craft::$app->getCache();
1154
        TagDependency::invalidate($cache, $this::GLOBAL_REDIRECTS_CACHE_TAG);
1155
        // If they are using Craft 3.3 or later, clear the GraphQL caches too
1156
        if (Retour::$craft33) {
1157
            $gql = Craft::$app->getGql();
1158
            if (method_exists($gql, 'invalidateCaches')) {
1159
                $gql->invalidateCaches();
1160
            }
1161
        }
1162
        Craft::debug(
1163
            Craft::t(
1164
                'retour',
1165
                'All redirect caches cleared'
1166
            ),
1167
            __METHOD__
1168
        );
1169
    }
1170
1171
    /**
0 ignored issues
show
Coding Style introduced by
Missing short description in doc comment
Loading history...
1172
     * @param $uri
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
1173
     *
1174
     * @return bool
1175
     */
1176
    public function excludeUri($uri): bool
1177
    {
1178
        $uri = '/' . ltrim($uri, '/');
1179
        if (!empty(Retour::$settings->excludePatterns)) {
1180
            foreach (Retour::$settings->excludePatterns as $excludePattern) {
1181
                $pattern = '`' . $excludePattern['pattern'] . '`i';
1182
                try {
1183
                    if (preg_match($pattern, $uri) === 1) {
1184
                        return true;
1185
                    }
1186
                } catch (\Exception $e) {
1187
                    // That's fine
1188
                    Craft::error('Invalid exclude URI Regex: ' . $pattern, __METHOD__);
1189
                }
1190
            }
1191
        }
1192
1193
        return false;
1194
    }
1195
1196
    /**
0 ignored issues
show
Coding Style introduced by
Parameter $request should have a doc-comment as per coding-style.
Loading history...
1197
     * Return whether this is a preview request of any kind
1198
     *
1199
     * @return bool
1200
     */
1201
    public function isPreview($request): bool
1202
    {
1203
        $isPreview = false;
1204
        if (Retour::$craft32) {
1205
            $isPreview = $request->getIsPreview();
1206
        }
1207
        $isLivePreview = $request->getIsLivePreview();
1208
1209
        return ($isPreview || $isLivePreview);
1210
    }
1211
}
1212