Passed
Push — develop ( 1cdd96...bece07 )
by Andrew
19:05 queued 09:51
created

UrlHelper::mergeUrlWithPath()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 18
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 20

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 13
c 1
b 0
f 0
dl 0
loc 18
ccs 0
cts 14
cp 0
rs 9.8333
cc 4
nc 3
nop 2
crap 20
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
        $urlLength = strlen($url);
77
        $urlOffset = $urlLength;
78
        $pathLength = strlen($path);
79
        $pathOffset = 0;
80
        while ($urlOffset > 0 && $pathOffset < $pathLength) {
81
            $urlOffset--;
82
            $pathOffset++;
83
            if (str_starts_with($path, substr($url, $urlOffset, $pathOffset))) {
84
                $overlap = $pathOffset;
85
            }
86
        }
87
88
        return $url . '/' . ltrim(substr($path, $overlap), '/');
89
    }
90
91
    /**
92
     * Return the page trigger and the value of the page trigger (null if it doesn't exist)
93
     *
94
     * @return array
95
     */
96
    public static function pageTriggerValue(): array
97
    {
98
        $pageTrigger = Craft::$app->getConfig()->getGeneral()->pageTrigger;
99
        if (!\is_string($pageTrigger) || $pageTrigger === '') {
100
            $pageTrigger = 'p';
101
        }
102
        // Is this query string-based pagination?
103
        if ($pageTrigger[0] === '?') {
104
            $pageTrigger = trim($pageTrigger, '?=');
105
        }
106
        // Avoid conflict with the path param
107
        $pathParam = Craft::$app->getConfig()->getGeneral()->pathParam;
108
        if ($pageTrigger === $pathParam) {
109
            $pageTrigger = $pathParam === 'p' ? 'pg' : 'p';
110
        }
111
        $pageTriggerValue = Craft::$app->getRequest()->getParam($pageTrigger);
112
113
        return [$pageTrigger, $pageTriggerValue];
114
    }
115
116
    /**
117
     * Return an absolute URL with protocol that curl will be happy with
118
     *
119
     * @param string $url
120
     *
121
     * @return string
122
     */
123
    public static function absoluteUrlWithProtocol($url): string
124
    {
125
        // Make this a full URL
126
        if (!self::isAbsoluteUrl($url)) {
127
            $protocol = 'http';
128
            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...
129
                || isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && strcasecmp($_SERVER['HTTP_X_FORWARDED_PROTO'], 'https') === 0
130
            ) {
131
                $protocol = 'https';
132
            }
133
            if (self::isProtocolRelativeUrl($url)) {
134
                try {
135
                    $url = self::urlWithScheme($url, $protocol);
136
                } catch (SiteNotFoundException $e) {
137
                    Craft::error($e->getMessage(), __METHOD__);
138
                }
139
            } else {
140
                try {
141
                    $url = self::siteUrl($url, null, $protocol);
142
                    if (self::isProtocolRelativeUrl($url)) {
143
                        $url = self::urlWithScheme($url, $protocol);
144
                    }
145
                } catch (Exception $e) {
146
                    Craft::error($e->getMessage(), __METHOD__);
147
                }
148
            }
149
        }
150
        // Ensure that any spaces in the URL are encoded
151
        $url = str_replace(' ', '%20', $url);
152
153
        // Handle trailing slashes properly for generated URLs
154
        $generalConfig = Craft::$app->getConfig()->getGeneral();
155
        if ($generalConfig->addTrailingSlashesToUrls && !preg_match('/\.[^\/]+$/', $url)) {
156
            $url = rtrim($url, '/') . '/';
157
        }
158
        if (!$generalConfig->addTrailingSlashesToUrls) {
159
            $url = rtrim($url, '/');
160
        }
161
162
        return DynamicMeta::sanitizeUrl($url, false, false);
163
    }
164
165
    /**
166
     * urlencode() just the query parameters in the URL
167
     *
168
     * @param string $url
169
     * @return string
170
     */
171 2
    public static function encodeUrlQueryParams(string $url): string
172
    {
173 2
        $urlParts = parse_url($url);
174 2
        $encodedUrl = "";
175 2
        if (isset($urlParts['scheme'])) {
176
            $encodedUrl .= $urlParts['scheme'] . '://';
177
        }
178 2
        if (isset($urlParts['host'])) {
179
            $encodedUrl .= $urlParts['host'];
180
        }
181 2
        if (isset($urlParts['port'])) {
182
            $encodedUrl .= ':' . $urlParts['port'];
183
        }
184 2
        if (isset($urlParts['path'])) {
185 2
            $encodedUrl .= $urlParts['path'];
186
        }
187 2
        if (isset($urlParts['query'])) {
188
            $query = explode('&', $urlParts['query']);
189
            foreach ($query as $j => $value) {
190
                $value = explode('=', $value, 2);
191
                if (count($value) === 2) {
192
                    $query[$j] = urlencode($value[0]) . '=' . urlencode($value[1]);
193
                } else {
194
                    $query[$j] = urlencode($value[0]);
195
                }
196
            }
197
            $encodedUrl .= '?' . implode('&', $query);
198
        }
199 2
        if (isset($urlParts['fragment'])) {
200
            $encodedUrl .= '#' . $urlParts['fragment'];
201
        }
202
203 2
        return $encodedUrl;
204
    }
205
206
    /**
207
     * Return whether this URL has a sub-directory as part of it
208
     *
209
     * @param string $url
210
     * @return bool
211
     */
212
    public static function urlHasSubDir(string $url): bool
213
    {
214
        return !empty(parse_url(trim($url, '/'), PHP_URL_PATH));
215
    }
216
217
    /**
218
     * Return the siteUrlOverride setting, which can be a string or an array of site URLs
219
     * indexed by the site handle
220
     *
221
     * @param int|null $siteId
222
     * @return string
223
     * @throws Exception
224
     * @throws SiteNotFoundException
225
     */
226
    public static function getSiteUrlOverrideSetting(?int $siteId = null): string
227
    {
228
        // If the override is a string, just return it
229
        $siteUrlOverride = Seomatic::$settings->siteUrlOverride;
230
        if (is_string($siteUrlOverride)) {
231
            return $siteUrlOverride;
232
        }
233
        // If the override is an array, pluck the appropriate one by handle
234
        if (is_array($siteUrlOverride)) {
0 ignored issues
show
introduced by
The condition is_array($siteUrlOverride) is always true.
Loading history...
235
            $sites = Craft::$app->getSites();
236
            $site = $sites->getCurrentSite();
237
            if ($siteId !== null) {
238
                $site = $sites->getSiteById($siteId, true);
239
                if (!$site) {
240
                    throw new Exception('Invalid site ID: ' . $siteId);
241
                }
242
            }
243
244
            return $siteUrlOverride[$site->handle] ?? '';
245
        }
246
247
        return '';
248
    }
249
250
    // Protected Methods
251
    // =========================================================================
252
253
    /**
254
     * Decompose a url into a prefix, path, and suffix
255
     *
256
     * @param $pathOrUrl
257
     *
258
     * @return array
259
     */
260
    protected static function decomposeUrl($pathOrUrl): array
261
    {
262
        $result = array();
263
264
        if (filter_var($pathOrUrl, FILTER_VALIDATE_URL)) {
265
            $url_parts = parse_url($pathOrUrl);
266
            $result['prefix'] = $url_parts['scheme'] . '://' . $url_parts['host'];
267
            $result['path'] = $url_parts['path'] ?? '';
268
            $result['suffix'] = '';
269
            $result['suffix'] .= empty($url_parts['query']) ? '' : '?' . $url_parts['query'];
270
            $result['suffix'] .= empty($url_parts['fragment']) ? '' : '#' . $url_parts['fragment'];
271
        } else {
272
            $result['prefix'] = '';
273
            $result['path'] = $pathOrUrl;
274
            $result['suffix'] = '';
275
        }
276
277
        return $result;
278
    }
279
}
280