Passed
Push — v3 ( 2b006a...43f239 )
by Andrew
33:34 queued 16:32
created

RedirectsController::addSlashToSiteUrls()   A

Complexity

Conditions 6
Paths 4

Size

Total Lines 18
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 42

Importance

Changes 2
Bugs 0 Features 2
Metric Value
eloc 8
c 2
b 0
f 2
dl 0
loc 18
ccs 0
cts 14
cp 0
rs 9.2222
cc 6
nc 4
nop 1
crap 42
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\controllers;
13
14
use nystudio107\retour\Retour;
15
use nystudio107\retour\assetbundles\retour\RetourAsset;
16
use nystudio107\retour\assetbundles\retour\RetourRedirectsAsset;
17
use nystudio107\retour\helpers\MultiSite as MultiSiteHelper;
18
use nystudio107\retour\helpers\Permission as PermissionHelper;
19
use nystudio107\retour\models\StaticRedirects as StaticRedirectsModel;
20
21
use Craft;
22
use craft\web\Controller;
23
use craft\helpers\UrlHelper;
24
25
use yii\base\InvalidConfigException;
26
use yii\web\NotFoundHttpException;
27
use yii\web\Response;
28
29
/**
0 ignored issues
show
Coding Style introduced by
Missing short description in doc comment
Loading history...
30
 * @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...
31
 * @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...
32
 * @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...
33
 */
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...
34
class RedirectsController extends Controller
35
{
36
    // Constants
37
    // =========================================================================
38
39
    const DOCUMENTATION_URL = 'https://github.com/nystudio107/craft-retour/';
40
41
    // Protected Properties
42
    // =========================================================================
43
44
    protected $allowAnonymous = [];
45
46
    // Public Methods
47
    // =========================================================================
48
49
    /**
50
     * Show the redirects table
51
     *
52
     * @param string|null $siteHandle
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
53
     *
54
     * @return Response
55
     * @throws \yii\web\ForbiddenHttpException
56
     * @throws \yii\web\NotFoundHttpException
57
     */
58
    public function actionRedirects(string $siteHandle = null): Response
59
    {
60
        $variables = [];
61
        PermissionHelper::controllerPermissionCheck('retour:redirects');
62
        // Get the site to edit
63
        $siteId = MultiSiteHelper::getSiteIdFromHandle($siteHandle);
64
        $pluginName = Retour::$settings->pluginName;
65
        $templateTitle = Craft::t('retour', 'Redirects');
66
        $view = Craft::$app->getView();
67
        // Asset bundle
68
        try {
69
            $view->registerAssetBundle(RetourRedirectsAsset::class);
70
        } catch (InvalidConfigException $e) {
71
            Craft::error($e->getMessage(), __METHOD__);
72
        }
73
        $variables['baseAssetsUrl'] = Craft::$app->assetManager->getPublishedUrl(
74
            '@nystudio107/retour/assetbundles/retour/dist',
75
            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

75
        /** @scrutinizer ignore-call */ 
76
        $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...
76
        );
77
        // Enabled sites
78
        MultiSiteHelper::setMultiSiteVariables($siteHandle, $siteId, $variables);
79
        $variables['controllerHandle'] = 'redirects';
80
81
        // Basic variables
82
        $variables['fullPageForm'] = false;
83
        $variables['docsUrl'] = self::DOCUMENTATION_URL;
84
        $variables['pluginName'] = $pluginName;
85
        $variables['title'] = $templateTitle;
86
        $siteHandleUri = Craft::$app->isMultiSite ? '/'.$siteHandle : '';
87
        $variables['crumbs'] = [
88
            [
89
                'label' => $pluginName,
90
                'url' => UrlHelper::cpUrl('retour'),
91
            ],
92
            [
93
                'label' => $templateTitle,
94
                'url' => UrlHelper::cpUrl('retour/redirects'.$siteHandleUri),
95
            ],
96
        ];
97
        $variables['docTitle'] = "{$pluginName} - {$templateTitle}";
98
        $variables['selectedSubnavItem'] = 'redirects';
99
100
        // Render the template
101
        return $this->renderTemplate('retour/redirects/index', $variables);
102
    }
103
104
    /**
105
     * Edit the redirect
106
     *
107
     * @param int                       $redirectId
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
108
     * @param string                    $defaultUrl
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
109
     * @param int                       $siteId
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
110
     * @param null|StaticRedirectsModel $redirect
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
111
     *
112
     * @return Response
113
     * @throws NotFoundHttpException
114
     * @throws \yii\web\ForbiddenHttpException
115
     */
116
    public function actionEditRedirect(
117
        int $redirectId = 0,
118
        string $defaultUrl = '',
119
        int $siteId = 0,
120
        StaticRedirectsModel $redirect = null
121
    ): Response {
122
        $variables = [];
123
        PermissionHelper::controllerPermissionCheck('retour:redirects');
124
125
        // Load in the redirect
126
        if ($redirectId === 0) {
127
            $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...
128
                'id' => 0,
129
                'siteId' => $siteId,
130
                'redirectSrcUrl' => urldecode($defaultUrl),
131
            ]);
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...
132
        }
133
        if ($redirect === null) {
134
            $redirectConfig = Retour::$plugin->redirects->getRedirectById($redirectId);
135
            if ($redirectConfig === null) {
136
                $redirectConfig = [];
137
                Craft::error(
138
                    Craft::t(
139
                        'retour',
140
                        "Couldn't load redirect id {id}",
141
                        ['id' => $redirectId]
142
                    ),
143
                    __METHOD__
144
                );
145
            }
146
            $redirect = new StaticRedirectsModel($redirectConfig);
147
        }
148
        $redirect->validate();
149
        // Ensure the user has permissions to edit this redirect
150
        $sites = Craft::$app->getSites();
151
        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...
152
            $site = $sites->getSiteById($redirect->siteId);
153
            if ($site) {
154
                MultiSiteHelper::requirePermission('editSite:'.$site->uid);
155
            }
156
        }
157
        if ($siteId) {
158
            $site = $sites->getSiteById($siteId);
159
            if ($site) {
160
                MultiSiteHelper::requirePermission('editSite:'.$site->uid);
161
            }
162
        }
163
        $pluginName = Retour::$settings->pluginName;
164
        $templateTitle = Craft::t('retour', 'Edit Redirect');
165
        $view = Craft::$app->getView();
166
        // Asset bundle
167
        try {
168
            $view->registerAssetBundle(RetourAsset::class);
169
        } catch (InvalidConfigException $e) {
170
            Craft::error($e->getMessage(), __METHOD__);
171
        }
172
        $variables['baseAssetsUrl'] = Craft::$app->assetManager->getPublishedUrl(
173
            '@nystudio107/retour/assetbundles/retour/dist',
174
            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

174
        /** @scrutinizer ignore-call */ 
175
        $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...
175
        );
176
        // Sites menu
177
        MultiSiteHelper::setSitesMenuVariables($variables);
178
        $variables['controllerHandle'] = 'redirects';
179
180
        // Basic variables
181
        $variables['fullPageForm'] = true;
182
        $variables['docsUrl'] = self::DOCUMENTATION_URL;
183
        $variables['pluginName'] = $pluginName;
184
        $variables['title'] = $templateTitle;
185
        $variables['crumbs'] = [
186
            [
187
                'label' => $pluginName,
188
                'url' => UrlHelper::cpUrl('retour'),
189
            ],
190
            [
191
                'label' => 'Redirects',
192
                'url' => UrlHelper::cpUrl('retour/redirects'),
193
            ],
194
            [
195
                'label' => $templateTitle,
196
                'url' => UrlHelper::cpUrl('retour/edit-redirect/'.$redirectId),
197
            ],
198
        ];
199
        $variables['docTitle'] = "{$pluginName} - Redirects - {$templateTitle}";
200
        $variables['selectedSubnavItem'] = 'redirects';
201
        $variables['redirect'] = $redirect;
202
203
        // Render the template
204
        return $this->renderTemplate('retour/redirects/_edit', $variables);
205
    }
206
207
    /**
0 ignored issues
show
Coding Style introduced by
Missing short description in doc comment
Loading history...
208
     * @return Response|void
209
     * @throws \craft\errors\MissingComponentException
210
     * @throws \yii\web\ForbiddenHttpException
211
     */
212
    public function actionDeleteRedirects()
213
    {
214
        PermissionHelper::controllerPermissionCheck('retour:redirects');
215
        $request = Craft::$app->getRequest();
216
        $redirectIds = $request->getRequiredBodyParam('redirectIds');
217
        $stickyError = false;
218
        foreach ($redirectIds as $redirectId) {
219
            if (Retour::$plugin->redirects->deleteRedirectById($redirectId) === 0) {
220
                $stickyError = true;
221
            }
222
        }
223
        Retour::$plugin->clearAllCaches();
224
        // Handle any cumulative errors
225
        if (!$stickyError) {
226
            // Clear the caches and continue on
227
            Craft::$app->getSession()->setNotice(Craft::t('retour', 'Retour redirects deleted.'));
228
229
            return $this->redirect('retour/redirects');
230
        }
231
        Craft::$app->getSession()->setError(Craft::t('retour', "Couldn't delete redirect."));
232
233
        return;
234
    }
235
236
    /**
237
     * Save the redirect
238
     *
239
     * @return null|Response
240
     * @throws \craft\errors\MissingComponentException
241
     * @throws \yii\web\BadRequestHttpException
242
     * @throws \yii\web\ForbiddenHttpException
243
     * @throws NotFoundHttpException
244
     */
245
    public function actionSaveRedirect()
246
    {
247
        PermissionHelper::controllerPermissionCheck('retour:redirects');
248
        $this->requirePostRequest();
249
        /** @var StaticRedirectsModel $redirect */
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...
250
        $redirectConfig = Craft::$app->getRequest()->getRequiredBodyParam('redirectConfig');
251
        if ($redirectConfig === null) {
252
            throw new NotFoundHttpException('Redirect not found');
253
        }
254
        $redirectConfig['id'] = (int)$redirectConfig['id'];
255
        // Handle enforcing trailing slashes
256
        $generalConfig = Craft::$app->getConfig()->getGeneral();
257
        if ($generalConfig->addTrailingSlashesToUrls && $redirectConfig['redirectMatchType'] === 'exactmatch') {
258
            $destUrl = $redirectConfig['redirectDestUrl'] ?? '';
259
            $redirectConfig['redirectDestUrl'] = $this->addSlashToSiteUrls($destUrl);
260
        }
261
        // Handle URL encoded URLs by decoding them before saving them
262
        if ($redirectConfig['redirectMatchType'] === 'exactmatch') {
263
            $redirectConfig['redirectSrcUrl'] = urldecode($redirectConfig['redirectSrcUrl'] ?? '');
264
            $redirectConfig['redirectSrcUrlParsed'] = urldecode($redirectConfig['redirectSrcUrlParsed'] ?? '');
265
        }
266
        $redirect = new StaticRedirectsModel($redirectConfig);
267
        // Make sure the redirect validates
268
        if (!$redirect->validate()) {
269
            Craft::$app->getSession()->setError(Craft::t('app', "Couldn't save redirect settings."));
270
            // Send the redirect back to the template
271
            Craft::$app->getUrlManager()->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...
272
                'redirect' => $redirect,
273
            ]);
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...
274
275
            return null;
276
        }
277
        // Save the redirect
278
        $redirectConfig = $redirect->getAttributes();
279
        Retour::$plugin->redirects->saveRedirect($redirectConfig);
280
        // Handle the case where the redirect wasn't saved because it'd create a redirect loop
281
        $testRedirectConfig = Retour::$plugin->redirects->getRedirectByRedirectSrcUrl(
282
            $redirectConfig['redirectSrcUrl'],
283
            $redirectConfig['siteId']
284
        );
285
        if ($testRedirectConfig === null) {
286
            Craft::$app->getSession()->setError(Craft::t('app', "Couldn't save redirect settings because it'd create a redirect loop."));
287
            // Send the redirect back to the template
288
            Craft::$app->getUrlManager()->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...
289
                'redirect' => $redirect,
290
            ]);
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...
291
292
            return null;
293
        }
294
        // Clear the caches and continue on
295
        Retour::$plugin->clearAllCaches();
296
        Craft::$app->getSession()->setNotice(Craft::t('retour', 'Redirect settings saved.'));
297
298
        return $this->redirectToPostedUrl();
299
    }
300
301
    // Protected Methods
302
    // =========================================================================
303
304
    /**
305
     * If the $url appears to be a site URL, add a slash to the end of it
306
     *
307
     * @param string $url
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
308
     *
309
     * @return string
310
     */
311
    protected function addSlashToSiteUrls(string $url): string
312
    {
313
        // Make sure the URL doesn't end with a file extension, e.g.: .jpg or have a query string
314
        if (!preg_match('/\.[^\/]+$/', $url) && strpos($url, '?') === false) {
315
            // If it's a root relative URL, assume it's a site URL
316
            if (UrlHelper::isRootRelativeUrl($url)) {
317
                return rtrim($url, '/') . '/';
318
            }
319
            // If the URL matches any of the site's base URLs, assume it's a site URL
320
            $sites = Craft::$app->getSites()->getAllSites();
321
            foreach ($sites as $site) {
322
                if (strpos($url, $site->getBaseUrl()) === 0) {
323
                    return rtrim($url, '/') . '/';
324
                }
325
            }
326
        }
327
328
        return $url;
329
    }
330
}
331