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 +107 -5 1.0.02.7.0 View file →
@@ -88,9 +88,9 @@
88 88 *
89 89 * @since 1.0.0
90 90 */
91 91 public function __construct() {
92 - $this->settings = new Settings();
92 + $this->settings = Settings::instance();
93 93 }
94 94
95 95 /**
96 96 * Register API routes
@@ -164,10 +164,30 @@
164 164 'message' => 'Invalid settings data provided'
165 165 ], 400);
166 166 }
167 167
168 - // Sanitize and save settings
168 + // Sanitize input, then enforce the shared format rules before saving.
169 + // Sanitization only strips unsafe characters; without this, malformed
170 + // verification codes / IDs would be stored and later emitted verbatim.
169 171 $sanitized_settings = $this->sanitize_settings($settings);
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 +
181 + $validation_errors = $this->validate_settings_format($sanitized_settings);
182 + if (!empty($validation_errors)) {
183 + return new WP_REST_Response([
184 + 'success' => false,
185 + 'message' => 'One or more social platform values are in an invalid format',
186 + 'errors' => $validation_errors
187 + ], 400);
188 + }
189 +
170 190 $success = $this->save_social_platform_settings($sanitized_settings);
171 191
172 192 if ($success) {
173 193 return new WP_REST_Response([
@@ -228,9 +248,9 @@
228 248 // Save each setting individually using the Settings class
229 249 // This ensures proper encryption for sensitive verification codes
230 250 foreach ($settings as $key => $value) {
231 251 // Only save if the key is in our allowed lists and has a value
232 - 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)) {
233 253 if (!$this->settings->set($key, $value)) {
234 254 $success = false;
235 255 }
236 256 }
@@ -266,8 +286,90 @@
266 286 return $sanitized;
267 287 }
268 288
269 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);
344 + }
345 +
346 + /**
347 + * Validate sanitized settings against the shared social platform format rules.
348 + *
349 + * Reuses Social_Meta_Manager's per-field rules (single source of truth) so the
350 + * REST save path rejects malformed verification codes / IDs instead of storing
351 + * them and letting them render as broken verification meta tags.
352 + *
353 + * @since 1.14.0
354 + *
355 + * @param array $settings Sanitized settings.
356 + * @return array<string, string> Map of field key => error message; empty when all valid.
357 + */
358 + private function validate_settings_format(array $settings): array {
359 + $errors = [];
360 +
361 + foreach ($settings as $key => $value) {
362 + $error = \ThinkRank\SEO\Social_Meta_Manager::validate_platform_field($key, $value);
363 + if ($error !== null) {
364 + $errors[$key] = $error;
365 + }
366 + }
367 +
368 + return $errors;
369 + }
370 +
371 + /**
270 372 * Mask verification code for security display (XXX pattern)
271 373 *
272 374 * @since 1.0.0
273 375 * @param string $code Verification code to mask
@@ -308,9 +410,9 @@
308 410 * @since 1.0.0
309 411 * @return bool Permission status
310 412 */
311 413 public function check_read_permissions(): bool {
312 - return current_user_can('manage_options');
414 + return \ThinkRank\Core\Capability_Manager::current_user_can('thinkrank_settings');
313 415 }
314 416
315 417 /**
316 418 * Check manage permissions
@@ -318,7 +420,7 @@
318 420 * @since 1.0.0
319 421 * @return bool Permission status
320 422 */
321 423 public function check_manage_permissions(): bool {
322 - return current_user_can('manage_options');
424 + return \ThinkRank\Core\Capability_Manager::current_user_can('thinkrank_settings');
323 425 }
324 426 }