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.8.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 All 49 releases
← All changes | includes/api/class-social-platforms-endpoint.php +66 -1 1.25.02.7.0 View file →
@@ -169,8 +169,16 @@
169 169 // Sanitization only strips unsafe characters; without this, malformed
170 170 // verification codes / IDs would be stored and later emitted verbatim.
171 171 $sanitized_settings = $this->sanitize_settings($settings);
172 172
173 + // Drop sensitive verification codes that arrive still masked (the
174 + // client resending the XXXX placeholder for an unchanged field).
175 + // The server is the source of truth: a masked value is never a real
176 + // edit, so removing it here both preserves the stored secret and
177 + // keeps the untouched placeholder out of the format validator below
178 + // (otherwise every save of this tab would 400 once a code is set).
179 + $sanitized_settings = $this->strip_masked_sensitive_values($sanitized_settings);
180 +
173 181 $validation_errors = $this->validate_settings_format($sanitized_settings);
174 182 if (!empty($validation_errors)) {
175 183 return new WP_REST_Response([
176 184 'success' => false,
@@ -240,9 +248,9 @@
240 248 // Save each setting individually using the Settings class
241 249 // This ensures proper encryption for sensitive verification codes
242 250 foreach ($settings as $key => $value) {
243 251 // Only save if the key is in our allowed lists and has a value
244 - if (in_array($key, array_merge($this->public_keys, $this->sensitive_keys)) && !empty($value)) {
252 + if (in_array($key, array_merge($this->public_keys, $this->sensitive_keys), true) && !empty($value)) {
245 253 if (!$this->settings->set($key, $value)) {
246 254 $success = false;
247 255 }
248 256 }
@@ -275,8 +283,65 @@
275 283 }
276 284 }
277 285
278 286 return $sanitized;
287 + }
288 +
289 + /**
290 + * Remove sensitive verification codes that are still masked.
291 + *
292 + * The Social Platforms tab receives verification codes masked (e.g. `a1b2XXXX`)
293 + * and binds them straight into their input fields. When the tab is saved
294 + * without re-typing a code, that masked placeholder is sent back. Persisting
295 + * it would overwrite the real encrypted secret, and — since 1.14.0 — it also
296 + * fails format validation, causing the whole save (including unrelated fields)
297 + * to 400. A masked value is never a genuine edit, so we drop it here and keep
298 + * the currently stored secret untouched.
299 + *
300 + * @since 1.27.0
301 + *
302 + * @param array $settings Sanitized settings.
303 + * @return array Settings with masked sensitive values removed.
304 + */
305 + private function strip_masked_sensitive_values(array $settings): array {
306 + foreach ($this->sensitive_keys as $key) {
307 + if (!isset($settings[$key]) || $settings[$key] === '') {
308 + continue;
309 + }
310 +
311 + $incoming = (string) $settings[$key];
312 + $stored = (string) $this->settings->get($key, '');
313 +
314 + // Primary check: the incoming value is exactly the mask of the
315 + // currently stored secret (the untouched field round-tripping).
316 + // Fallback check: the value still matches a generic mask shape, so
317 + // even without a stored value we never persist a bare placeholder.
318 + if (
319 + ($stored !== '' && $incoming === $this->mask_verification_code($stored))
320 + || $this->is_masked_value($incoming)
321 + ) {
322 + unset($settings[$key]);
323 + }
324 + }
325 +
326 + return $settings;
327 + }
328 +
329 + /**
330 + * Determine whether a value looks like a masking placeholder.
331 + *
332 + * Mirrors the shapes produced by mask_verification_code(): `XXXX` for short
333 + * codes and `<first 4 chars>XXXX` for longer ones. Genuine verification codes
334 + * for the sensitive keys are never this short (Pinterest is 32 hex chars;
335 + * Instagram/TikTok require 20+ chars), so a match is safe to treat as "unchanged".
336 + *
337 + * @since 1.27.0
338 + *
339 + * @param string $value Value to test.
340 + * @return bool True when the value is a mask placeholder.
341 + */
342 + private function is_masked_value(string $value): bool {
343 + return $value === 'XXXX' || (bool) preg_match('/^.{4}XXXX$/', $value);
279 344 }
280 345
281 346 /**
282 347 * Validate sanitized settings against the shared social platform format rules.