Passed
Push — v3 ( 0d1d42...e1dcac )
by Andrew
32:56 queued 25:33
created

Redirects::deleteShortlinkById()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 13
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 12

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 8
c 1
b 0
f 0
dl 0
loc 13
ccs 0
cts 9
cp 0
rs 10
cc 3
nc 3
nop 1
crap 12
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
     * Updates an associated element short link value.
924
     *
925
     * @param array $redirectConfig
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
926
     * @param array $existingData
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
927
     */
0 ignored issues
show
Coding Style introduced by
Missing @return tag in function comment
Loading history...
928
    protected function updateAssociatedElementShortLink(array $redirectConfig, array $existingData)
929
    {
930
        if (empty($redirectConfig['associatedElementId'])) {
931
            return;
932
        }
933
        // Get the element and set the scenario
934
        $associatedElement = Craft::$app->getElements()->getElementById($redirectConfig['associatedElementId']);
935
936
        if (!$associatedElement) {
937
            return;
938
        }
939
940
        $fieldUpdated = $this->setShortLinkFieldValue($associatedElement, $existingData['redirectSrcUrl'], $redirectConfig['redirectSrcUrl']);
941
942
        if ($fieldUpdated) {
943
            // Prevent element from triggering an infinite loop.
944
            ShortLink::preventShortLinkUpdates();
945
            Craft::$app->getElements()->saveElement($associatedElement);
946
            ShortLink::allowShortLinkUpdates();
947
        }
948
    }
949
950
    /**
951
     * Save an element redirect.
952
     *
953
     * @param ElementInterface $element
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
954
     * @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...
955
     * @param string $redirectSrcMatch
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...
956
     * @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...
957
     */
0 ignored issues
show
Coding Style introduced by
Missing @return tag in function comment
Loading history...
958
    public function enableElementRedirect(ElementInterface $element, string $sourceUrl, string $redirectSrcMatch = 'pathonly', int $redirectHttpCode = 301)
959
    {
960
        $siteId = $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...
961
        $parentElement = ElementHelper::rootElement($element);
962
963
        $redirectConfig = [
964
            'redirectMatchType' => 'exactmatch',
965
            'redirectSrcUrl' => $sourceUrl,
966
            'siteId' => $siteId,
967
            'associatedElementId' => $element->getCanonicalId(),
968
            'enabled' => $parentElement->getEnabledForSite($siteId),
969
            'redirectSrcMatch' => $redirectSrcMatch,
970
            '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...
971
            'redirectHttpCode' => $redirectHttpCode,
972
        ];
973
974
        $this->saveRedirect($redirectConfig);
975
    }
976
977
    /**
0 ignored issues
show
Coding Style introduced by
Parameter $allSites should have a doc-comment as per coding-style.
Loading history...
978
     * Delete an element redirect.
979
     *
980
     * @param ElementInterface $element
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
981
     */
0 ignored issues
show
Coding Style introduced by
Missing @return tag in function comment
Loading history...
982
    public function removeElementRedirect(ElementInterface $element, bool $allSites = false)
983
    {
984
        $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...
985
986
        if (!empty($redirects)) {
987
            foreach ($redirects as $redirect) {
988
                $this->deleteRedirectById($redirect['id']);
989
            }
990
        }
991
    }
992
993
    /**
994
     * Delete a short link by its ID.
995
     *
996
     * @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...
997
     * @throws \Throwable
0 ignored issues
show
Coding Style introduced by
Tag @throws cannot be grouped with parameter tags in a doc comment
Loading history...
998
     * @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...
999
     * @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...
1000
     */
0 ignored issues
show
Coding Style introduced by
Missing @return tag in function comment
Loading history...
1001
    public function deleteShortlinkById(int $redirectId)
1002
    {
1003
        $redirect = $this->getRedirectById($redirectId);
1004
        $elementId = $redirect['associatedElementId'];
1005
        $siteId = $redirect['siteId'];
1006
        $element = Craft::$app->getElements()->getElementById($elementId, null, $siteId);
1007
1008
        if ($element) {
1009
            $fieldUpdated = $this->setShortLinkFieldValue($element, $redirect['redirectSrcUrl'], null);
1010
1011
            if ($fieldUpdated) {
1012
                // This will also delete the redirect from the table
1013
                Craft::$app->getElements()->saveElement($element);
1014
            }
1015
        }
1016
    }
1017
1018
    /**
0 ignored issues
show
Coding Style introduced by
Missing short description in doc comment
Loading history...
1019
     * @param array $redirectConfig
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...
1020
     * @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...
1021
     */
1022
    public function saveRedirect(array $redirectConfig): bool
1023
    {
1024
        // Handle URL encoded URLs by decoding them before saving them
1025
        if (isset($redirectConfig['redirectMatchType']) && $redirectConfig['redirectMatchType'] === 'exactmatch') {
1026
            $redirectConfig['redirectSrcUrl'] = urldecode($redirectConfig['redirectSrcUrl'] ?? '');
1027
            $redirectConfig['redirectSrcUrlParsed'] = urldecode($redirectConfig['redirectSrcUrlParsed'] ?? '');
1028
        }
1029
        // Validate the model before saving it to the db
1030
        $redirect = new StaticRedirectsModel($redirectConfig);
1031
        if ($redirect->validate() === false) {
1032
            Craft::error(
1033
                Craft::t(
1034
                    'retour',
1035
                    'Error validating redirect {id}: {errors}',
1036
                    ['id' => $redirect->id, 'errors' => print_r($redirect->getErrors(), true)]
1037
                ),
1038
                __METHOD__
1039
            );
1040
1041
            return false;
1042
        }
1043
        // Get the validated model attributes and save them to the db
1044
        $redirectConfig = $redirect->getAttributes();
1045
        // 0 for a siteId needs to be converted to null
1046
        if (empty($redirectConfig['siteId']) || (int)$redirectConfig['siteId'] === 0) {
1047
            $redirectConfig['siteId'] = null;
1048
        }
1049
        // Throw an event to before saving the redirect
1050
        $db = Craft::$app->getDb();
1051
        if (!empty($redirectConfig['associatedElementId'])) {
1052
            $existingRedirect = (new Query())->from(['{{%retour_static_redirects}}'])->where(['id' => $redirectConfig['id']])->one();
1053
        }
1054
1055
        // See if a redirect exists with this source URL already
1056
        if ((int)$redirectConfig['id'] === 0) {
1057
            // Query the db table
1058
            $redirect = (new Query())
1059
                ->from(['{{%retour_static_redirects}}'])
1060
                ->where(['redirectSrcUrlParsed' => $redirectConfig['redirectSrcUrlParsed']])
1061
                ->andWhere(['siteId' => $redirectConfig['siteId']])
1062
                ->one();
1063
            // If it exists, update it rather than having duplicates
1064
            if (!empty($redirect)) {
1065
                $redirectConfig['id'] = $redirect['id'];
1066
            }
1067
        }
1068
        // Trigger a 'beforeSaveRedirect' event
1069
        $isNew = (int)$redirectConfig['id'] === 0;
1070
        $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...
1071
            'isNew' => $isNew,
1072
            'legacyUrl' => $redirectConfig['redirectSrcUrlParsed'],
1073
            'destinationUrl' => $redirectConfig['redirectDestUrl'],
1074
            'matchType' => $redirectConfig['redirectSrcMatch'],
1075
            'redirectType' => $redirectConfig['redirectHttpCode'],
1076
            'siteId' => $redirectConfig['siteId'],
1077
        ]);
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...
1078
        $this->trigger(self::EVENT_BEFORE_SAVE_REDIRECT, $event);
1079
        if (!$event->isValid) {
1080
            return false;
1081
        }
1082
        // See if this is an existing redirect
1083
        if (!$isNew) {
1084
            Craft::debug(
1085
                Craft::t(
1086
                    'retour',
1087
                    'Updating existing redirect: {redirect}',
1088
                    ['redirect' => print_r($redirectConfig, true)]
1089
                ),
1090
                __METHOD__
1091
            );
1092
            // Update the existing record
1093
            try {
1094
                $db->createCommand()->update(
1095
                    '{{%retour_static_redirects}}',
1096
                    $redirectConfig,
1097
                    [
1098
                        'id' => $redirectConfig['id'],
1099
                    ]
1100
                )->execute();
1101
            } catch (Exception $e) {
1102
                Craft::error($e->getMessage(), __METHOD__);
1103
1104
                return false;
1105
            }
1106
        } else {
1107
            Craft::debug(
1108
                Craft::t(
1109
                    'retour',
1110
                    'Creating new redirect: {redirect}',
1111
                    ['redirect' => print_r($redirectConfig, true)]
1112
                ),
1113
                __METHOD__
1114
            );
1115
            unset($redirectConfig['id']);
1116
            // Create a new record
1117
            try {
1118
                $db->createCommand()->insert(
1119
                    '{{%retour_static_redirects}}',
1120
                    $redirectConfig
1121
                )->execute();
1122
            } catch (Exception $e) {
1123
                Craft::error($e->getMessage(), __METHOD__);
1124
1125
                return false;
1126
            }
1127
        }
1128
        // To prevent redirect loops, see if any static redirects have our redirectDestUrl as their redirectSrcUrl
1129
        $testRedirectConfig = $this->getRedirectByRedirectSrcUrl(
1130
            $redirectConfig['redirectDestUrl'],
1131
            $redirectConfig['siteId']
1132
        );
1133
        if ($testRedirectConfig !== null) {
1134
            Craft::debug(
1135
                Craft::t(
1136
                    'retour',
1137
                    'Deleting redirect to prevent a loop: {redirect}',
1138
                    ['redirect' => print_r($testRedirectConfig, true)]
1139
                ),
1140
                __METHOD__
1141
            );
1142
            // Delete the redirect that has a redirectSrcUrl the same as this record's redirectDestUrl
1143
            try {
1144
                $db->createCommand()->delete(
1145
                    '{{%retour_static_redirects}}',
1146
                    ['id' => $testRedirectConfig['id']]
1147
                )->execute();
1148
            } catch (Exception $e) {
1149
                Craft::error($e->getMessage(), __METHOD__);
1150
            }
1151
        }
1152
        // Trigger a 'afterSaveRedirect' event
1153
        $this->trigger(self::EVENT_AFTER_SAVE_REDIRECT, $event);
1154
1155
        if (!empty($redirectConfig['associatedElementId']) && !empty($existingRedirect)) {
1156
            $this->updateAssociatedElementShortLink($redirectConfig, $existingRedirect);
1157
        }
1158
1159
        // Invalidate caches after saving a redirect
1160
        $this->invalidateCaches();
1161
1162
        return true;
1163
    }
1164
1165
    /**
0 ignored issues
show
Coding Style introduced by
Missing short description in doc comment
Loading history...
1166
     * @param $uri
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
1167
     *
1168
     * @return bool
1169
     */
1170
    public function excludeUri($uri): bool
1171
    {
1172
        $uri = '/' . ltrim($uri, '/');
1173
        if (!empty(Retour::$settings->excludePatterns)) {
1174
            foreach (Retour::$settings->excludePatterns as $excludePattern) {
1175
                $pattern = '`' . $excludePattern['pattern'] . '`i';
1176
                try {
1177
                    if (preg_match($pattern, $uri) === 1) {
1178
                        return true;
1179
                    }
1180
                } catch (\Exception $e) {
1181
                    // That's fine
1182
                    Craft::error('Invalid exclude URI Regex: ' . $pattern, __METHOD__);
1183
                }
1184
            }
1185
        }
1186
1187
        return false;
1188
    }
1189
1190
    /**
1191
     * Invalidate all of the redirects caches
1192
     */
0 ignored issues
show
Coding Style introduced by
Missing @return tag in function comment
Loading history...
1193
    public function invalidateCaches()
1194
    {
1195
        $cache = Craft::$app->getCache();
1196
        TagDependency::invalidate($cache, $this::GLOBAL_REDIRECTS_CACHE_TAG);
1197
        // If they are using Craft 3.3 or later, clear the GraphQL caches too
1198
        if (Retour::$craft33) {
1199
            $gql = Craft::$app->getGql();
1200
            if (method_exists($gql, 'invalidateCaches')) {
1201
                $gql->invalidateCaches();
1202
            }
1203
        }
1204
        Craft::debug(
1205
            Craft::t(
1206
                'retour',
1207
                'All redirect caches cleared'
1208
            ),
1209
            __METHOD__
1210
        );
1211
    }
1212
1213
    /**
0 ignored issues
show
Coding Style introduced by
Parameter $request should have a doc-comment as per coding-style.
Loading history...
1214
     * Return whether this is a preview request of any kind
1215
     *
1216
     * @return bool
1217
     */
1218
    public function isPreview($request): bool
1219
    {
1220
        $isPreview = false;
1221
        if (Retour::$craft32) {
1222
            $isPreview = $request->getIsPreview();
1223
        }
1224
        $isLivePreview = $request->getIsLivePreview();
1225
1226
        return ($isPreview || $isLivePreview);
1227
    }
1228
1229
    /**
1230
     * Find all short link fields on an element that have a matching redirect source url and update the value
1231
     *
1232
     * @param ElementInterface $element
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...
1233
     * @param string $redirectSrcUrl
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...
Coding Style introduced by
Tag value for @param tag indented incorrectly; expected 2 spaces but found 1
Loading history...
1234
     * @param mixed $newValue
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
Coding Style introduced by
Expected 12 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...
1235
     * @return bool whether a match was found
0 ignored issues
show
Coding Style introduced by
Tag @return cannot be grouped with parameter tags in a doc comment
Loading history...
1236
     */
1237
    protected function setShortLinkFieldValue(ElementInterface $element, string $redirectSrcUrl, $newValue): bool
1238
    {
1239
        $layout = $element->getFieldLayout();
1240
        $srcUrl = $redirectSrcUrl;
1241
        $site = $element->getSite();
1242
        $urlLess = StringHelper::removeLeft($srcUrl, $site->getBaseUrl());
0 ignored issues
show
Bug introduced by
It seems like $site->getBaseUrl() can also be of type null; however, parameter $substring of craft\helpers\StringHelper::removeLeft() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

1242
        $urlLess = StringHelper::removeLeft($srcUrl, /** @scrutinizer ignore-type */ $site->getBaseUrl());
Loading history...
1243
1244
        $matchFound = false;
1245
        foreach ($element->getFieldValues() as $fieldHandle => $fieldValue) {
1246
            if (!is_string($fieldValue)) {
1247
                continue;
1248
            }
1249
            // Compare field value with starting slashes dropped to the redirectSrcUrl value as well as one with site URL removed, just in case
1250
            if (in_array(ltrim($fieldValue, '/'), [ltrim($srcUrl, '/'), ltrim($urlLess, '/')], true)) {
1251
                $field = $layout->getFieldByHandle($fieldHandle);
1252
1253
                if ($field instanceof ShortLink) {
1254
                    $element->setFieldValue($fieldHandle, $newValue);
1255
                    $matchFound = true;
1256
                }
1257
            }
1258
        }
1259
1260
        return $matchFound;
1261
    }
1262
}
1263