PluginProbe
Search Atlas SEO – OTTO AI SEO Automation for WordPress / 2.7.0
Search Atlas SEO – OTTO AI SEO Automation for WordPress v2.7.0
2.7.1 2.7.2 2.7.0 2.6.26 2.6.25 2.6.24 2.6.23 2.6.22 2.6.21 2.6.20 2.6.19 2.6.18 2.6.17 2.6.16 2.6.15 2.6.14 2.6.13 2.6.12 2.6.11 2.6.10 2.6.9 2.6.8 2.6.7 2.6.6 2.6.5 All 141 releases
metasync / includes / class-metasync-canonical-sanitizer.php

class-metasync-canonical-sanitizer.php in Search Atlas SEO – OTTO AI SEO Automation for WordPress 2.7.0, at includes/class-metasync-canonical-sanitizer.php

157 lines 5.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Canonical URL sanitizer.
4 *
5 * Central guard against the legacy meta_canonical corruption where array
6 * values were cast to the literal string "Array" and later emitted as
7 * <link rel="canonical" href="http://Array">. Every read, write,
8 * and sync of a canonical value routes through this class so a corrupted
9 * database row can never reach page output or third-party SEO storage.
10 *
11 * @package Search Atlas SEO
12 */
13
14 if (!defined('ABSPATH')) {
15 exit;
16 }
17
18 if (!class_exists('Metasync_Canonical_Sanitizer')) {
19
20 class Metasync_Canonical_Sanitizer {
21
22 /**
23 * Dig the first scalar out of a possibly-nested array value.
24 *
25 * Legacy sanitize_array() storage sometimes produced nested arrays;
26 * a single reset() on those returns another array, and a subsequent
27 * (string) cast produces the literal "Array" — the original source
28 * of the corruption. This walks down to the first scalar instead.
29 *
30 * @param mixed $value Raw meta / input value.
31 * @return string|int|float|bool|'' First scalar found, or '' when none exists.
32 */
33 public static function extract_scalar($value) {
34 $depth = 10; // hard cap — corrupt data must never loop forever
35 while (is_array($value) && $depth-- > 0) {
36 $value = reset($value);
37 }
38 // reset([]) yields false — booleans are never a usable canonical.
39 return (is_scalar($value) && !is_bool($value)) ? $value : '';
40 }
41
42 /**
43 * Whether a value is one of the known corruption artifacts.
44 *
45 * "Array" is what PHP produces when an array is cast to string;
46 * "http://Array" / "https://Array" are what esc_url_raw() makes of it.
47 *
48 * @param mixed $value Value to test (non-strings are never corrupted literals).
49 * @return bool
50 */
51 public static function is_corrupted($value) {
52 if (!is_string($value)) {
53 return false;
54 }
55 $normalized = strtolower(rtrim(trim($value), '/'));
56 return in_array($normalized, array('array', 'http://array', 'https://array'), true);
57 }
58
59 /**
60 * Validate a stored canonical value for output or sync.
61 *
62 * Accepts absolute http/https URLs and site-relative paths ("/about/"),
63 * both of which rel=canonical allows and prior plugin versions emitted.
64 * Rejects arrays (after first-scalar extraction), the corruption
65 * literals, and anything that is not a well-formed URL — returning ''
66 * so callers fall back to their default (usually the permalink),
67 * matching pre-2.6.16 behavior for posts with no usable canonical.
68 *
69 * Deliberately NOT wp_http_validate_url(): that helper is an SSRF
70 * guard for outbound requests and rejects legitimate canonical hosts
71 * (non-standard ports, intranet hostnames) that sites do publish.
72 *
73 * @param mixed $value Raw meta / input value.
74 * @return string Valid canonical URL, or '' when the value is unusable.
75 */
76 public static function sanitize($value) {
77 $value = self::extract_scalar($value);
78 if (!is_string($value) && !is_numeric($value)) {
79 return '';
80 }
81 $value = trim((string) $value);
82 if ($value === '' || self::is_corrupted($value)) {
83 return '';
84 }
85
86 // Site-relative canonical ("/path/") — allowed; exclude
87 // protocol-relative "//host" which FILTER_VALIDATE_URL can't vet.
88 if ($value[0] === '/') {
89 return (!isset($value[1]) || $value[1] !== '/') ? $value : '';
90 }
91
92 // Legacy rows stored before scheme normalization ("example.com/x",
93 // e.g. via the old REST sanitize_text_field path): emit them the way
94 // esc_url() always did — http:// prepended — but only for values
95 // with a dotted, colon-free host so bare words stay rejected.
96 if (!preg_match('#^[a-zA-Z][a-zA-Z0-9+.\-]*://#', $value)) {
97 $host_part = strstr($value, '/', true);
98 $host_part = ($host_part === false) ? $value : $host_part;
99 if (strpos($host_part, ':') !== false || strpos($host_part, '.') === false) {
100 return '';
101 }
102 $value = 'http://' . $value;
103 }
104
105 return self::is_valid_http_url($value) ? $value : '';
106 }
107
108 /**
109 * Structural URL check tolerant of the UTF-8 canonicals esc_url() emits.
110 *
111 * FILTER_VALIDATE_URL is ASCII-only, so IDN hosts and unencoded UTF-8
112 * paths (both previously stored and emitted by this plugin) would fail
113 * it. Validate a copy with high bytes mapped to a safe ASCII letter;
114 * genuinely malformed ASCII (spaces, missing host, bad scheme) still
115 * fails as it should.
116 *
117 * @param string $value Candidate URL (scheme already present).
118 * @return bool
119 */
120 private static function is_valid_http_url($value) {
121 $ascii = preg_replace('/[\x80-\xFF]/', 'x', $value);
122 if (!filter_var($ascii, FILTER_VALIDATE_URL)) {
123 return false;
124 }
125 $scheme = strtolower((string) parse_url($ascii, PHP_URL_SCHEME));
126 return $scheme === 'http' || $scheme === 'https';
127 }
128
129 /**
130 * Sanitize user/API input before persisting it as a canonical.
131 *
132 * Same rules as sanitize(); esc_url_raw() (when available) additionally
133 * normalizes/cleans the value the way the meta box always did, and
134 * sanitize()'s own schemeless handling keeps behavior deterministic in
135 * environments without WordPress loaded. Returns '' for unusable input —
136 * callers should then delete the meta rather than store garbage.
137 *
138 * @param mixed $value Raw user/API input.
139 * @return string Storable canonical URL, or '' when input is unusable.
140 */
141 public static function sanitize_for_save($value) {
142 $value = self::extract_scalar($value);
143 if (!is_string($value) && !is_numeric($value)) {
144 return '';
145 }
146 $value = trim((string) $value);
147 if ($value === '' || self::is_corrupted($value)) {
148 return '';
149 }
150 if (function_exists('esc_url_raw')) {
151 $value = esc_url_raw($value);
152 }
153 return self::sanitize($value);
154 }
155 }
156 }
157