PluginProbe
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO / 2.7.0
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO v2.7.0
2.7.0 2.6.0 2.5.0 2.4.0 2.3.0 2.2.0 2.1.1 2.1.0 2.0.2 2.0.1 2.0.0 1.32.0 1.31.0 1.30.0 1.29.0 1.28.0 1.27.0 1.26.0 1.25.0 trunk 1.0.0 1.0.1 1.0.2 1.1.0 1.10.0 All 48 releases
thinkrank / includes / seo / class-url-scheme.php

class-url-scheme.php in ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO 2.7.0, at includes/seo/class-url-scheme.php

239 lines 6.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * The scheme ThinkRank publishes its own URLs with.
4 *
5 * @package ThinkRank
6 * @since 2.7.0
7 */
8
9 declare(strict_types=1);
10
11 namespace ThinkRank\SEO;
12
13 if (!defined('ABSPATH')) {
14 exit;
15 }
16
17 /**
18 * Decides whether a self-referential URL goes out as http or https.
19 *
20 * Sites behind a proxy or load balancer that terminates TLS hand PHP a plain
21 * HTTP request, so `is_ssl()` is false and WordPress falls back to whatever
22 * scheme `home` was stored with. On a site installed over http and later put
23 * behind TLS without anyone updating the option, every URL WordPress builds
24 * says `http://` while every visitor is on `https://` — and the canonical tag
25 * then tells search engines the wrong address for every page on the site.
26 *
27 * The site owner frequently cannot fix that at the server level, which is why
28 * this is a setting rather than a support answer.
29 *
30 * Two rules keep it from doing damage of its own:
31 *
32 * - **Only this site's own URLs are touched.** A cross-domain canonical the
33 * author typed, a `sameAs` pointing at a social profile, an image on a CDN:
34 * all of them are somebody else's URLs, and forcing our scheme onto them
35 * would break them. Host has to match before anything is rewritten.
36 * - **`automatic` changes nothing at all.** Not "resolves to the current
37 * scheme" — nothing. A site that never opens the setting emits exactly what
38 * WordPress reports, which is what every site did before this existed.
39 *
40 * @since 2.7.0
41 */
42 class Url_Scheme {
43
44 /**
45 * Follow whatever WordPress reports. The default.
46 *
47 * @since 2.7.0
48 * @var string
49 */
50 public const AUTOMATIC = 'automatic';
51
52 /**
53 * Publish every self-URL as https.
54 *
55 * @since 2.7.0
56 * @var string
57 */
58 public const HTTPS = 'https';
59
60 /**
61 * Publish every self-URL as http.
62 *
63 * @since 2.7.0
64 * @var string
65 */
66 public const HTTP = 'http';
67
68 /**
69 * The accepted values, in the order the settings screen lists them.
70 *
71 * @since 2.7.0
72 * @var string[]
73 */
74 public const MODES = [self::AUTOMATIC, self::HTTPS, self::HTTP];
75
76 /**
77 * Resolved preference for this request.
78 *
79 * @since 2.7.0
80 * @var string|null
81 */
82 private static $mode = null;
83
84 /**
85 * This site's host, lowercased.
86 *
87 * @since 2.7.0
88 * @var string|null
89 */
90 private static $host = null;
91
92 /**
93 * The scheme preference this site has stored.
94 *
95 * Read once per request. Every emit point calls this, including the
96 * sitemap generator inside a loop over thousands of URLs, so the settings
97 * read must not repeat.
98 *
99 * @since 2.7.0
100 * @return string One of MODES.
101 */
102 public static function preference(): string {
103 if (null === self::$mode) {
104 $stored = '';
105
106 if (class_exists('\ThinkRank\SEO\Site_Identity_Manager')) {
107 $settings = (new Site_Identity_Manager())->get_settings('site', null);
108 $stored = (string) ($settings['canonical_scheme'] ?? '');
109 }
110
111 /**
112 * Filter the scheme ThinkRank publishes its own URLs with.
113 *
114 * @since 2.7.0
115 *
116 * @param string $mode One of Url_Scheme::MODES.
117 */
118 $mode = (string) apply_filters('thinkrank_canonical_scheme', $stored);
119
120 self::$mode = in_array($mode, self::MODES, true) ? $mode : self::AUTOMATIC;
121 }
122
123 return self::$mode;
124 }
125
126 /**
127 * Apply the site's scheme preference to one URL.
128 *
129 * @since 2.7.0
130 * @param string $url Absolute URL, or anything else (returned untouched).
131 * @return string
132 */
133 public static function apply(string $url): string {
134 $mode = self::preference();
135
136 if (self::AUTOMATIC === $mode || '' === $url) {
137 return $url;
138 }
139
140 $scheme = wp_parse_url($url, PHP_URL_SCHEME);
141
142 // No scheme means relative or protocol-relative: not ours to decide.
143 if (!is_string($scheme) || '' === $scheme) {
144 return $url;
145 }
146
147 $scheme = strtolower($scheme);
148
149 if ('http' !== $scheme && 'https' !== $scheme) {
150 return $url;
151 }
152
153 if ($scheme === $mode || !self::is_own_url($url)) {
154 return $url;
155 }
156
157 return set_url_scheme($url, $mode);
158 }
159
160 /**
161 * Apply the preference to every string in a nested structure.
162 *
163 * The schema graph is the reason this exists: its `@id` and `url` values
164 * come from a dozen different producers, some deriving from WordPress and
165 * some from stored settings, and normalizing at the point of serialization
166 * is what stops one graph carrying both schemes at once.
167 *
168 * @since 2.7.0
169 * @param mixed $value Array, string, or anything else.
170 * @return mixed Same shape, schemes normalized.
171 */
172 public static function apply_deep($value) {
173 if (self::AUTOMATIC === self::preference()) {
174 return $value;
175 }
176
177 if (is_string($value)) {
178 return self::apply($value);
179 }
180
181 if (!is_array($value)) {
182 return $value;
183 }
184
185 foreach ($value as $key => $item) {
186 $value[$key] = self::apply_deep($item);
187 }
188
189 return $value;
190 }
191
192 /**
193 * Whether a URL belongs to this site.
194 *
195 * Host comparison only. A subdomain is a different site as far as a
196 * canonical is concerned, and a port or path difference is still us.
197 *
198 * @since 2.7.0
199 * @param string $url Absolute URL.
200 * @return bool
201 */
202 private static function is_own_url(string $url): bool {
203 $host = wp_parse_url($url, PHP_URL_HOST);
204
205 if (!is_string($host) || '' === $host) {
206 return false;
207 }
208
209 return strtolower($host) === self::host();
210 }
211
212 /**
213 * This site's host, lowercased and memoised.
214 *
215 * @since 2.7.0
216 * @return string
217 */
218 private static function host(): string {
219 if (null === self::$host) {
220 $host = wp_parse_url(home_url('/'), PHP_URL_HOST);
221
222 self::$host = is_string($host) ? strtolower($host) : '';
223 }
224
225 return self::$host;
226 }
227
228 /**
229 * Forget the memoised preference and host.
230 *
231 * @since 2.7.0
232 * @return void
233 */
234 public static function reset(): void {
235 self::$mode = null;
236 self::$host = null;
237 }
238 }
239