RedirectsController   A
last analyzed

Complexity

Total Complexity 36

Size/Duplication

Total Lines 388
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 36
eloc 182
c 0
b 0
f 0
dl 0
loc 388
rs 9.52

7 Methods

Rating   Name   Duplication   Size   Complexity  
A actionRedirects() 0 44 3
B actionEditRedirect() 0 89 9
B actionSaveRedirect() 0 57 7
A actionDeleteRedirects() 0 22 4
A addSlashToSiteUrls() 0 18 6
A actionShortlinks() 0 44 3
A actionDeleteShortlinks() 0 22 4
1
<?php
2
/**
3
 * Retour plugin for Craft CMS
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\controllers;
13
14
use Craft;
15
use craft\errors\ElementNotFoundException;
16
use craft\errors\MissingComponentException;
17
use craft\helpers\UrlHelper;
18
use craft\web\Controller;
19
use craft\web\UrlManager;
20
use nystudio107\retour\assetbundles\retour\RetourAsset;
21
use nystudio107\retour\assetbundles\retour\RetourRedirectsAsset;
22
use nystudio107\retour\helpers\MultiSite as MultiSiteHelper;
23
use nystudio107\retour\helpers\Permission as PermissionHelper;
24
use nystudio107\retour\models\StaticRedirects as StaticRedirectsModel;
25
use nystudio107\retour\Retour;
26
use yii\base\Exception;
27
use yii\base\InvalidConfigException;
28
use yii\web\BadRequestHttpException;
29
use yii\web\ForbiddenHttpException;
30
use yii\web\MethodNotAllowedHttpException;
31
use yii\web\NotFoundHttpException;
32
use yii\web\Response;
33
34
/**
0 ignored issues
show
Coding Style introduced by
Missing short description in doc comment
Loading history...
35
 * @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...
36
 * @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...
37
 * @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...
38
 */
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...
39
class RedirectsController extends Controller
40
{
41
    // Constants
42
    // =========================================================================
43
44
    protected const DOCUMENTATION_URL = 'https://github.com/nystudio107/craft-retour/';
45
46
    // Protected Properties
47
    // =========================================================================
48
49
    protected array|bool|int $allowAnonymous = [];
50
51
    // Public Methods
52
    // =========================================================================
53
54
    /**
55
     * Show the redirects table
56
     *
57
     * @param string|null $siteHandle
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
58
     *
59
     * @return Response
60
     * @throws ForbiddenHttpException
61
     * @throws NotFoundHttpException
62
     */
63
    public function actionRedirects(string $siteHandle = null): Response
64
    {
65
        $variables = [];
66
        PermissionHelper::controllerPermissionCheck('retour:redirects');
67
        // Get the site to edit
68
        $siteId = MultiSiteHelper::getSiteIdFromHandle($siteHandle);
69
        $pluginName = Retour::$settings->pluginName;
70
        $templateTitle = Craft::t('retour', 'Redirects');
71
        $view = Craft::$app->getView();
72
        // Asset bundle
73
        try {
74
            $view->registerAssetBundle(RetourRedirectsAsset::class);
75
        } catch (InvalidConfigException $e) {
76
            Craft::error($e->getMessage(), __METHOD__);
77
        }
78
        $variables['baseAssetsUrl'] = Craft::$app->assetManager->getPublishedUrl(
79
            '@nystudio107/retour/web/assets/dist',
80
            true
0 ignored issues
show
Unused Code introduced by
The call to yii\web\AssetManager::getPublishedUrl() has too many arguments starting with true. ( Ignorable by Annotation )

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

80
        /** @scrutinizer ignore-call */ 
81
        $variables['baseAssetsUrl'] = Craft::$app->assetManager->getPublishedUrl(

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
81
        );
82
        // Enabled sites
83
        MultiSiteHelper::setMultiSiteVariables($siteHandle, $siteId, $variables);
84
        $variables['controllerHandle'] = 'redirects';
85
86
        // Basic variables
87
        $variables['fullPageForm'] = false;
88
        $variables['docsUrl'] = self::DOCUMENTATION_URL;
89
        $variables['pluginName'] = $pluginName;
90
        $variables['title'] = $templateTitle;
91
        $siteHandleUri = Craft::$app->isMultiSite ? '/' . $siteHandle : '';
92
        $variables['crumbs'] = [
93
            [
94
                'label' => $pluginName,
95
                'url' => UrlHelper::cpUrl('retour'),
96
            ],
97
            [
98
                'label' => $templateTitle,
99
                'url' => UrlHelper::cpUrl('retour/redirects' . $siteHandleUri),
100
            ],
101
        ];
102
        $variables['docTitle'] = "{$pluginName} - {$templateTitle}";
103
        $variables['selectedSubnavItem'] = 'redirects';
104
105
        // Render the template
106
        return $this->renderTemplate('retour/redirects/index', $variables);
107
    }
108
109
    /**
110
     * Edit the redirect
111
     *
112
     * @param int $redirectId
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
Coding Style introduced by
Expected 23 spaces after parameter type; 1 found
Loading history...
113
     * @param string $defaultUrl
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
Coding Style introduced by
Expected 20 spaces after parameter type; 1 found
Loading history...
114
     * @param int $siteId
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
Coding Style introduced by
Expected 23 spaces after parameter type; 1 found
Loading history...
115
     * @param null|StaticRedirectsModel $redirect
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
116
     *
117
     * @return Response
118
     * @throws NotFoundHttpException
119
     * @throws ForbiddenHttpException
120
     */
121
    public function actionEditRedirect(
122
        int                  $redirectId = 0,
123
        string               $defaultUrl = '',
124
        int                  $siteId = 0,
125
        StaticRedirectsModel $redirect = null,
126
    ): Response {
127
        $variables = [];
128
        PermissionHelper::controllerPermissionCheck('retour:redirects');
129
130
        // Load in the redirect
131
        if ($redirectId === 0) {
132
            $redirect = 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...
133
                'id' => 0,
134
                'siteId' => $siteId,
135
                'redirectSrcUrl' => urldecode($defaultUrl),
136
            ]);
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...
137
        }
138
        if ($redirect === null) {
139
            $redirectConfig = Retour::$plugin->redirects->getRedirectById($redirectId);
0 ignored issues
show
Bug introduced by
The method getRedirectById() 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

139
            /** @scrutinizer ignore-call */ 
140
            $redirectConfig = Retour::$plugin->redirects->getRedirectById($redirectId);

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...
140
            if ($redirectConfig === null) {
141
                $redirectConfig = [];
142
                Craft::error(
143
                    Craft::t(
144
                        'retour',
145
                        "Couldn't load redirect id {id}",
146
                        ['id' => $redirectId]
147
                    ),
148
                    __METHOD__
149
                );
150
            }
151
            $redirect = new StaticRedirectsModel($redirectConfig);
152
        }
153
        $redirect->validate();
154
        // Ensure the user has permissions to edit this redirect
155
        $sites = Craft::$app->getSites();
156
        if ($redirect->siteId) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $redirect->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...
157
            $site = $sites->getSiteById($redirect->siteId);
158
            if ($site) {
159
                MultiSiteHelper::requirePermission('editSite:' . $site->uid);
160
            }
161
        }
162
        if ($siteId) {
163
            $site = $sites->getSiteById($siteId);
164
            if ($site) {
165
                MultiSiteHelper::requirePermission('editSite:' . $site->uid);
166
            }
167
        }
168
        $pluginName = Retour::$settings->pluginName;
169
        $templateTitle = Craft::t('retour', 'Edit Redirect');
170
        $view = Craft::$app->getView();
171
        // Asset bundle
172
        try {
173
            $view->registerAssetBundle(RetourAsset::class);
174
        } catch (InvalidConfigException $e) {
175
            Craft::error($e->getMessage(), __METHOD__);
176
        }
177
        $variables['baseAssetsUrl'] = Craft::$app->assetManager->getPublishedUrl(
178
            '@nystudio107/retour/web/assets/dist',
179
            true
0 ignored issues
show
Unused Code introduced by
The call to yii\web\AssetManager::getPublishedUrl() has too many arguments starting with true. ( Ignorable by Annotation )

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

179
        /** @scrutinizer ignore-call */ 
180
        $variables['baseAssetsUrl'] = Craft::$app->assetManager->getPublishedUrl(

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
180
        );
181
        // Sites menu
182
        MultiSiteHelper::setSitesMenuVariables($variables);
183
        $variables['controllerHandle'] = 'redirects';
184
185
        // Basic variables
186
        $variables['fullPageForm'] = true;
187
        $variables['docsUrl'] = self::DOCUMENTATION_URL;
188
        $variables['pluginName'] = $pluginName;
189
        $variables['title'] = $templateTitle;
190
        $variables['crumbs'] = [
191
            [
192
                'label' => $pluginName,
193
                'url' => UrlHelper::cpUrl('retour'),
194
            ],
195
            [
196
                'label' => 'Redirects',
197
                'url' => UrlHelper::cpUrl('retour/redirects'),
198
            ],
199
            [
200
                'label' => $templateTitle,
201
                'url' => UrlHelper::cpUrl('retour/edit-redirect/' . $redirectId),
202
            ],
203
        ];
204
        $variables['docTitle'] = "{$pluginName} - Redirects - {$templateTitle}";
205
        $variables['selectedSubnavItem'] = 'redirects';
206
        $variables['redirect'] = $redirect;
207
208
        // Render the template
209
        return $this->renderTemplate('retour/redirects/_edit', $variables);
210
    }
211
212
    /**
0 ignored issues
show
Coding Style introduced by
Missing short description in doc comment
Loading history...
213
     * @return ?Response
214
     * @throws MissingComponentException
215
     * @throws ForbiddenHttpException
216
     * @throws BadRequestHttpException
217
     */
218
    public function actionDeleteRedirects(): ?Response
219
    {
220
        PermissionHelper::controllerPermissionCheck('retour:redirects');
221
        $request = Craft::$app->getRequest();
222
        $redirectIds = $request->getRequiredBodyParam('redirectIds');
223
        $stickyError = false;
224
        foreach ($redirectIds as $redirectId) {
225
            if (Retour::$plugin->redirects->deleteRedirectById($redirectId) === 0) {
226
                $stickyError = true;
227
            }
228
        }
229
        Retour::$plugin->clearAllCaches();
0 ignored issues
show
Bug introduced by
The method clearAllCaches() 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

229
        Retour::$plugin->/** @scrutinizer ignore-call */ 
230
                         clearAllCaches();

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...
230
        // Handle any cumulative errors
231
        if (!$stickyError) {
232
            // Clear the caches and continue on
233
            Craft::$app->getSession()->setNotice(Craft::t('retour', 'Retour redirects deleted.'));
234
235
            return $this->redirect('retour/redirects');
236
        }
237
        Craft::$app->getSession()->setError(Craft::t('retour', "Couldn't delete redirect."));
238
239
        return null;
240
    }
241
242
    /**
243
     * Save the redirect
244
     *
245
     * @return null|Response
246
     * @throws MissingComponentException
247
     * @throws BadRequestHttpException
248
     * @throws ForbiddenHttpException
249
     * @throws NotFoundHttpException
250
     * @throws MethodNotAllowedHttpException
251
     */
252
    public function actionSaveRedirect(): ?Response
253
    {
254
        PermissionHelper::controllerPermissionCheck('retour:redirects');
255
        $this->requirePostRequest();
256
        $redirectConfig = Craft::$app->getRequest()->getRequiredBodyParam('redirectConfig');
257
        if ($redirectConfig === null) {
258
            throw new NotFoundHttpException('Redirect not found');
259
        }
260
        $redirectConfig['id'] = (int)$redirectConfig['id'];
261
        // Handle enforcing trailing slashes
262
        $generalConfig = Craft::$app->getConfig()->getGeneral();
263
        if ($generalConfig->addTrailingSlashesToUrls && $redirectConfig['redirectMatchType'] === 'exactmatch') {
264
            $destUrl = $redirectConfig['redirectDestUrl'] ?? '';
265
            $redirectConfig['redirectDestUrl'] = $this->addSlashToSiteUrls($destUrl);
266
        }
267
        // Handle URL encoded URLs by decoding them before saving them
268
        if ($redirectConfig['redirectMatchType'] === 'exactmatch') {
269
            $redirectConfig['redirectSrcUrl'] = urldecode($redirectConfig['redirectSrcUrl'] ?? '');
270
            $redirectConfig['redirectSrcUrlParsed'] = urldecode($redirectConfig['redirectSrcUrlParsed'] ?? '');
271
        }
272
        $redirect = new StaticRedirectsModel($redirectConfig);
273
        // Make sure the redirect validates
274
        if (!$redirect->validate()) {
275
            Craft::$app->getSession()->setError(Craft::t('app', "Couldn't save redirect settings."));
276
            // Send the redirect back to the template
277
            /** @var UrlManager $urlManager */
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...
278
            $urlManager = Craft::$app->getUrlManager();
279
            $urlManager->setRouteParams([
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...
280
                'redirect' => $redirect,
281
            ]);
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...
282
283
            return null;
284
        }
285
        // Save the redirect
286
        $redirectConfig = $redirect->getAttributes();
287
        Retour::$plugin->redirects->saveRedirect($redirectConfig);
288
        // Handle the case where the redirect wasn't saved because it'd create a redirect loop
289
        $testRedirectConfig = Retour::$plugin->redirects->getRedirectByRedirectSrcUrl(
290
            $redirectConfig['redirectSrcUrl'],
291
            $redirectConfig['siteId']
292
        );
293
        if ($testRedirectConfig === null) {
294
            Craft::$app->getSession()->setError(Craft::t('app', "Couldn't save redirect settings because it'd create a redirect loop."));
295
            // Send the redirect back to the template
296
            /** @var UrlManager $urlManager */
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...
297
            $urlManager = Craft::$app->getUrlManager();
298
            $urlManager->setRouteParams([
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...
299
                'redirect' => $redirect,
300
            ]);
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...
301
302
            return null;
303
        }
304
        // Clear the caches and continue on
305
        Retour::$plugin->clearAllCaches();
306
        Craft::$app->getSession()->setNotice(Craft::t('retour', 'Redirect settings saved.'));
307
308
        return $this->redirectToPostedUrl();
309
    }
310
311
    /**
312
     * Show the short links table
313
     *
314
     * @param string|null $siteHandle
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
315
     *
316
     * @return Response
317
     * @throws \yii\web\ForbiddenHttpException
318
     * @throws \yii\web\NotFoundHttpException
319
     */
320
    public function actionShortlinks(string $siteHandle = null): Response
321
    {
322
        $variables = [];
323
        PermissionHelper::controllerPermissionCheck('retour:shortlinks');
324
        // Get the site to edit
325
        $siteId = MultiSiteHelper::getSiteIdFromHandle($siteHandle);
326
        $pluginName = Retour::$settings->pluginName;
327
        $templateTitle = Craft::t('retour', 'Short Links');
328
        $view = Craft::$app->getView();
329
        // Asset bundle
330
        try {
331
            $view->registerAssetBundle(RetourRedirectsAsset::class);
332
        } catch (InvalidConfigException $e) {
333
            Craft::error($e->getMessage(), __METHOD__);
334
        }
335
        $variables['baseAssetsUrl'] = Craft::$app->assetManager->getPublishedUrl(
336
            '@nystudio107/retour/web/assets/dist',
337
            true
0 ignored issues
show
Unused Code introduced by
The call to yii\web\AssetManager::getPublishedUrl() has too many arguments starting with true. ( Ignorable by Annotation )

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

337
        /** @scrutinizer ignore-call */ 
338
        $variables['baseAssetsUrl'] = Craft::$app->assetManager->getPublishedUrl(

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
338
        );
339
        // Enabled sites
340
        MultiSiteHelper::setMultiSiteVariables($siteHandle, $siteId, $variables);
341
        $variables['controllerHandle'] = 'shortlinks';
342
343
        // Basic variables
344
        $variables['fullPageForm'] = false;
345
        $variables['docsUrl'] = self::DOCUMENTATION_URL;
346
        $variables['pluginName'] = $pluginName;
347
        $variables['title'] = $templateTitle;
348
        $siteHandleUri = Craft::$app->isMultiSite ? '/' . $siteHandle : '';
349
        $variables['crumbs'] = [
350
            [
351
                'label' => $pluginName,
352
                'url' => UrlHelper::cpUrl('retour'),
353
            ],
354
            [
355
                'label' => $templateTitle,
356
                'url' => UrlHelper::cpUrl('retour/shortlinks' . $siteHandleUri),
357
            ],
358
        ];
359
        $variables['docTitle'] = "{$pluginName} - {$templateTitle}";
360
        $variables['selectedSubnavItem'] = 'shortlinks';
361
362
        // Render the template
363
        return $this->renderTemplate('retour/shortlinks/index', $variables);
364
    }
365
366
    /**
0 ignored issues
show
Coding Style introduced by
Missing short description in doc comment
Loading history...
367
     * @return Response|void
368
     * @throws BadRequestHttpException
369
     * @throws ForbiddenHttpException
370
     * @throws MissingComponentException
371
     * @throws \Throwable
372
     * @throws ElementNotFoundException
373
     * @throws Exception
374
     */
375
    public function actionDeleteShortlinks()
376
    {
377
        PermissionHelper::controllerPermissionCheck('retour:shortlinks');
378
        $request = Craft::$app->getRequest();
379
        $redirectIds = $request->getRequiredBodyParam('redirectIds');
380
        $stickyError = false;
381
        foreach ($redirectIds as $redirectId) {
382
            if (Retour::$plugin->redirects->deleteShortlinkById($redirectId)) {
383
                $stickyError = true;
384
            }
385
        }
386
        Retour::$plugin->clearAllCaches();
387
        // Handle any cumulative errors
388
        if (!$stickyError) {
389
            // Clear the caches and continue on
390
            Craft::$app->getSession()->setNotice(Craft::t('retour', 'Retour Short Links deleted.'));
391
392
            return $this->redirect('retour/shortlinks');
393
        }
394
        Craft::$app->getSession()->setError(Craft::t('retour', "Couldn't delete Short Link."));
395
396
        return;
397
    }
398
399
    // Protected Methods
400
    // =========================================================================
401
402
    /**
403
     * If the $url appears to be a site URL, add a slash to the end of it
404
     *
405
     * @param string $url
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
406
     *
407
     * @return string
408
     */
409
    protected function addSlashToSiteUrls(string $url): string
410
    {
411
        // Make sure the URL doesn't end with a file extension, e.g.: .jpg or have a query string
412
        if (!preg_match('/\.[^\/]+$/', $url) && strpos($url, '?') === false) {
413
            // If it's a root relative URL, assume it's a site URL
414
            if (UrlHelper::isRootRelativeUrl($url)) {
415
                return rtrim($url, '/') . '/';
416
            }
417
            // If the URL matches any of the site's base URLs, assume it's a site URL
418
            $sites = Craft::$app->getSites()->getAllSites();
419
            foreach ($sites as $site) {
420
                if (strpos($url, $site->getBaseUrl()) === 0) {
421
                    return rtrim($url, '/') . '/';
422
                }
423
            }
424
        }
425
426
        return $url;
427
    }
428
}
429