Passed
Push — develop ( bece07...355eb3 )
by Andrew
08:58
created

UrlHelper   B

Complexity

Total Complexity 49

Size/Duplication

Total Lines 252
Duplicated Lines 0 %

Test Coverage

Coverage 9.9%

Importance

Changes 4
Bugs 2 Features 0
Metric Value
wmc 49
eloc 110
c 4
b 2
f 0
dl 0
loc 252
ccs 11
cts 111
cp 0.099
rs 8.48

8 Methods

Rating   Name   Duplication   Size   Complexity  
A siteUrl() 0 26 6
B encodeUrlQueryParams() 0 33 9
A mergeUrlWithPath() 0 17 4
A getSiteUrlOverrideSetting() 0 22 5
C absoluteUrlWithProtocol() 0 40 14
A decomposeUrl() 0 18 4
A pageTriggerValue() 0 18 6
A urlHasSubDir() 0 3 1

How to fix   Complexity   

Complex Class

Complex classes like UrlHelper often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use UrlHelper, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * SEOmatic plugin for Craft CMS 3.x
4
 *
5
 * A turnkey SEO implementation for Craft CMS that is comprehensive, powerful,
6
 * and flexible
7
 *
8
 * @link      https://nystudio107.com
9
 * @copyright Copyright (c) 2017 nystudio107
10
 */
11
12
namespace nystudio107\seomatic\helpers;
13
14
use Craft;
15
use craft\errors\SiteNotFoundException;
16
use craft\helpers\UrlHelper as CraftUrlHelper;
17
use nystudio107\seomatic\Seomatic;
18
use yii\base\Exception;
19
20
/**
21
 * @author    nystudio107
22
 * @package   Seomatic
23
 * @since     3.0.0
24
 */
25
class UrlHelper extends CraftUrlHelper
26
{
27
    // Public Static Properties
28
    // =========================================================================
29
30
    // Public Static Methods
31
    // =========================================================================
32
33
    /**
34
     * @inheritDoc
35
     */
36
    public static function siteUrl(string $path = '', $params = null, string $scheme = null, int $siteId = null): string
37
    {
38
        try {
39
            $siteUrl = self::getSiteUrlOverrideSetting($siteId);
40
        } catch (\Throwable $e) {
41
            // That's okay
42
        }
43
        if (!empty($siteUrl)) {
44
            $siteUrl = MetaValue::parseString($siteUrl);
45
            // Extract out just the path part
46
            $parts = self::decomposeUrl($path);
47
            $path = $parts['path'] . $parts['suffix'];
48
            $url = self::mergeUrlWithPath($siteUrl, $path);
49
            // Handle trailing slashes properly for generated URLs
50
            $generalConfig = Craft::$app->getConfig()->getGeneral();
51
            if ($generalConfig->addTrailingSlashesToUrls && !preg_match('/\.[^\/]+$/', $url)) {
52
                $url = rtrim($url, '/') . '/';
53
            }
54
            if (!$generalConfig->addTrailingSlashesToUrls) {
55
                $url = rtrim($url, '/');
56
            }
57
58
            return DynamicMeta::sanitizeUrl(parent::urlWithParams($url, $params ?? []), false, false);
59
        }
60
61
        return DynamicMeta::sanitizeUrl(parent::siteUrl($path, $params, $scheme, $siteId), false, false);
62
    }
63
64
    /**
65
     * Merge the $url and $path together, combining any overlapping path segments
66
     *
67
     * @param string $url
68
     * @param string $path
69
     * @return string
70
     */
71
    public static function mergeUrlWithPath(string $url, string $path): string
72
    {
73
        $overlap = 0;
74
        $url = rtrim($url, '/');
75
        $path = ltrim($path, '/');
76
        $urlOffset = strlen($url);
77
        $pathLength = strlen($path);
78
        $pathOffset = 0;
79
        while ($urlOffset > 0 && $pathOffset < $pathLength) {
80
            $urlOffset--;
81
            $pathOffset++;
82
            if (str_starts_with($path, substr($url, $urlOffset, $pathOffset))) {
83
                $overlap = $pathOffset;
84
            }
85
        }
86
87
        return $url . '/' . ltrim(substr($path, $overlap), '/');
88
    }
89
90
    /**
91
     * Return the page trigger and the value of the page trigger (null if it doesn't exist)
92
     *
93
     * @return array
94
     */
95
    public static function pageTriggerValue(): array
96
    {
97
        $pageTrigger = Craft::$app->getConfig()->getGeneral()->pageTrigger;
98
        if (!\is_string($pageTrigger) || $pageTrigger === '') {
99
            $pageTrigger = 'p';
100
        }
101
        // Is this query string-based pagination?
102
        if ($pageTrigger[0] === '?') {
103
            $pageTrigger = trim($pageTrigger, '?=');
104
        }
105
        // Avoid conflict with the path param
106
        $pathParam = Craft::$app->getConfig()->getGeneral()->pathParam;
107
        if ($pageTrigger === $pathParam) {
108
            $pageTrigger = $pathParam === 'p' ? 'pg' : 'p';
109
        }
110
        $pageTriggerValue = Craft::$app->getRequest()->getParam($pageTrigger);
111
112
        return [$pageTrigger, $pageTriggerValue];
113
    }
114
115
    /**
116
     * Return an absolute URL with protocol that curl will be happy with
117
     *
118
     * @param string $url
119
     *
120
     * @return string
121
     */
122
    public static function absoluteUrlWithProtocol($url): string
123
    {
124
        // Make this a full URL
125
        if (!self::isAbsoluteUrl($url)) {
126
            $protocol = 'http';
127
            if (isset($_SERVER['HTTPS']) && (strcasecmp($_SERVER['HTTPS'], 'on') === 0 || $_SERVER['HTTPS'] == 1)
0 ignored issues
show
introduced by
Consider adding parentheses for clarity. Current Interpretation: (IssetNode && strcasecmp...PROTO'], 'https') === 0, Probably Intended Meaning: IssetNode && (strcasecmp...ROTO'], 'https') === 0)
Loading history...
128
                || isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && strcasecmp($_SERVER['HTTP_X_FORWARDED_PROTO'], 'https') === 0
129
            ) {
130
                $protocol = 'https';
131
            }
132
            if (self::isProtocolRelativeUrl($url)) {
133
                try {
134
                    $url = self::urlWithScheme($url, $protocol);
135
                } catch (SiteNotFoundException $e) {
136
                    Craft::error($e->getMessage(), __METHOD__);
137
                }
138
            } else {
139
                try {
140
                    $url = self::siteUrl($url, null, $protocol);
141
                    if (self::isProtocolRelativeUrl($url)) {
142
                        $url = self::urlWithScheme($url, $protocol);
143
                    }
144
                } catch (Exception $e) {
145
                    Craft::error($e->getMessage(), __METHOD__);
146
                }
147
            }
148
        }
149
        // Ensure that any spaces in the URL are encoded
150
        $url = str_replace(' ', '%20', $url);
151
152
        // Handle trailing slashes properly for generated URLs
153
        $generalConfig = Craft::$app->getConfig()->getGeneral();
154
        if ($generalConfig->addTrailingSlashesToUrls && !preg_match('/\.[^\/]+$/', $url)) {
155
            $url = rtrim($url, '/') . '/';
156
        }
157
        if (!$generalConfig->addTrailingSlashesToUrls) {
158
            $url = rtrim($url, '/');
159
        }
160
161
        return DynamicMeta::sanitizeUrl($url, false, false);
162
    }
163
164
    /**
165
     * urlencode() just the query parameters in the URL
166
     *
167
     * @param string $url
168
     * @return string
169
     */
170 2
    public static function encodeUrlQueryParams(string $url): string
171
    {
172 2
        $urlParts = parse_url($url);
173 2
        $encodedUrl = "";
174 2
        if (isset($urlParts['scheme'])) {
175
            $encodedUrl .= $urlParts['scheme'] . '://';
176
        }
177 2
        if (isset($urlParts['host'])) {
178
            $encodedUrl .= $urlParts['host'];
179
        }
180 2
        if (isset($urlParts['port'])) {
181
            $encodedUrl .= ':' . $urlParts['port'];
182
        }
183 2
        if (isset($urlParts['path'])) {
184 2
            $encodedUrl .= $urlParts['path'];
185
        }
186 2
        if (isset($urlParts['query'])) {
187
            $query = explode('&', $urlParts['query']);
188
            foreach ($query as $j => $value) {
189
                $value = explode('=', $value, 2);
190
                if (count($value) === 2) {
191
                    $query[$j] = urlencode($value[0]) . '=' . urlencode($value[1]);
192
                } else {
193
                    $query[$j] = urlencode($value[0]);
194
                }
195
            }
196
            $encodedUrl .= '?' . implode('&', $query);
197
        }
198 2
        if (isset($urlParts['fragment'])) {
199
            $encodedUrl .= '#' . $urlParts['fragment'];
200
        }
201
202 2
        return $encodedUrl;
203
    }
204
205
    /**
206
     * Return whether this URL has a sub-directory as part of it
207
     *
208
     * @param string $url
209
     * @return bool
210
     */
211
    public static function urlHasSubDir(string $url): bool
212
    {
213
        return !empty(parse_url(trim($url, '/'), PHP_URL_PATH));
214
    }
215
216
    /**
217
     * Return the siteUrlOverride setting, which can be a string or an array of site URLs
218
     * indexed by the site handle
219
     *
220
     * @param int|null $siteId
221
     * @return string
222
     * @throws Exception
223
     * @throws SiteNotFoundException
224
     */
225
    public static function getSiteUrlOverrideSetting(?int $siteId = null): string
226
    {
227
        // If the override is a string, just return it
228
        $siteUrlOverride = Seomatic::$settings->siteUrlOverride;
229
        if (is_string($siteUrlOverride)) {
230
            return $siteUrlOverride;
231
        }
232
        // If the override is an array, pluck the appropriate one by handle
233
        if (is_array($siteUrlOverride)) {
0 ignored issues
show
introduced by
The condition is_array($siteUrlOverride) is always true.
Loading history...
234
            $sites = Craft::$app->getSites();
235
            $site = $sites->getCurrentSite();
236
            if ($siteId !== null) {
237
                $site = $sites->getSiteById($siteId, true);
238
                if (!$site) {
239
                    throw new Exception('Invalid site ID: ' . $siteId);
240
                }
241
            }
242
243
            return $siteUrlOverride[$site->handle] ?? '';
244
        }
245
246
        return '';
247
    }
248
249
    // Protected Methods
250
    // =========================================================================
251
252
    /**
253
     * Decompose a url into a prefix, path, and suffix
254
     *
255
     * @param $pathOrUrl
256
     *
257
     * @return array
258
     */
259
    protected static function decomposeUrl($pathOrUrl): array
260
    {
261
        $result = array();
262
263
        if (filter_var($pathOrUrl, FILTER_VALIDATE_URL)) {
264
            $url_parts = parse_url($pathOrUrl);
265
            $result['prefix'] = $url_parts['scheme'] . '://' . $url_parts['host'];
266
            $result['path'] = $url_parts['path'] ?? '';
267
            $result['suffix'] = '';
268
            $result['suffix'] .= empty($url_parts['query']) ? '' : '?' . $url_parts['query'];
269
            $result['suffix'] .= empty($url_parts['fragment']) ? '' : '#' . $url_parts['fragment'];
270
        } else {
271
            $result['prefix'] = '';
272
            $result['path'] = $pathOrUrl;
273
            $result['suffix'] = '';
274
        }
275
276
        return $result;
277
    }
278
}
279