| @@ -15,8 +15,9 @@ | ||
| 15 | 15 | declare(strict_types=1); |
| 16 | 16 | |
| 17 | 17 | namespace ThinkRank\API; |
| 18 | 18 | |
| 19 | +use ThinkRank\SEO\Pattern_Resolver; | |
| 19 | 20 | use WP_REST_Controller; |
| 20 | 21 | use WP_REST_Request; |
| 21 | 22 | use WP_REST_Response; |
| 22 | 23 | use WP_Error; |
| @@ -238,18 +239,28 @@ | ||
| 238 | 239 | // Capture the previously-stored value so a genuine write failure can be |
| 239 | 240 | // told apart from a no-op save (payload identical to what's stored). |
| 240 | 241 | $previous = $all_settings[$post_type] ?? null; |
| 241 | 242 | |
| 243 | + // MERGE, never replace. This entity row is shared: the Content Type | |
| 244 | + // Matrix stores its per-feature tri-states (meta_enabled, schema_enabled, | |
| 245 | + // open_graph_enabled, twitter_card_enabled, analytics_enabled) and its | |
| 246 | + // sitemap_include flag under the SAME key, and normalize_settings_patch() | |
| 247 | + // whitelists only the Global SEO fields — so a wholesale replace here | |
| 248 | + // silently dropped every matrix choice the moment the user saved the | |
| 249 | + // Global SEO screen next door. Global SEO's own keys still win, because | |
| 250 | + // sanitize_settings() emits them complete. | |
| 251 | + $merged = array_merge(is_array($previous) ? $previous : [], $sanitized_settings); | |
| 252 | + | |
| 242 | 253 | // Update settings for this post type |
| 243 | - $all_settings[$post_type] = $sanitized_settings; | |
| 254 | + $all_settings[$post_type] = $merged; | |
| 244 | 255 | |
| 245 | 256 | // Save to database |
| 246 | 257 | $updated = update_option(self::OPTION_NAME, $all_settings); |
| 247 | 258 | |
| 248 | - if ($updated || $previous === $sanitized_settings) { | |
| 259 | + if ($updated || $previous === $merged) { | |
| 249 | 260 | return new WP_REST_Response([ |
| 250 | 261 | 'success' => true, |
| 251 | - 'data' => $sanitized_settings, | |
| 262 | + 'data' => $merged, | |
| 252 | 263 | 'post_type' => $post_type, |
| 253 | 264 | 'message' => sprintf('Settings saved successfully for post type: %s', $post_type) |
| 254 | 265 | ], 200); |
| 255 | 266 | } |
| @@ -299,11 +310,22 @@ | ||
| 299 | 310 | |
| 300 | 311 | // Get all settings |
| 301 | 312 | $all_settings = get_option(self::OPTION_NAME, []); |
| 302 | 313 | |
| 303 | - // Remove settings for this post type (will fall back to defaults) | |
| 304 | - unset($all_settings[$post_type]); | |
| 314 | + // Reset only what this screen owns. The entity row is shared with the | |
| 315 | + // Content Type Matrix, so dropping the whole row would reset the user's | |
| 316 | + // per-feature tri-states and sitemap choice as a side effect of a button | |
| 317 | + // that says nothing about them. DEFAULT_SETTINGS is the exact set of | |
| 318 | + // Global SEO keys, so anything outside it belongs to another screen. | |
| 319 | + $stored = $all_settings[$post_type] ?? []; | |
| 320 | + $kept = is_array($stored) ? array_diff_key($stored, self::DEFAULT_SETTINGS) : []; | |
| 305 | 321 | |
| 322 | + if ($kept === []) { | |
| 323 | + unset($all_settings[$post_type]); | |
| 324 | + } else { | |
| 325 | + $all_settings[$post_type] = $kept; | |
| 326 | + } | |
| 327 | + | |
| 306 | 328 | // Save updated settings |
| 307 | 329 | update_option(self::OPTION_NAME, $all_settings); |
| 308 | 330 | |
| 309 | 331 | // Get default settings |
| @@ -471,13 +493,15 @@ | ||
| 471 | 493 | */ |
| 472 | 494 | public static function normalize_settings_patch(array $settings): array { |
| 473 | 495 | $out = []; |
| 474 | 496 | |
| 497 | + // Templates, not plain text: sanitize_text_field() would eat %date% and | |
| 498 | + // %category% as percent-encoding and store "te%" / "tegory%" (#521). | |
| 475 | 499 | if (isset($settings['title'])) { |
| 476 | - $out['title'] = sanitize_text_field((string) $settings['title']); | |
| 500 | + $out['title'] = Pattern_Resolver::sanitize_template((string) $settings['title']); | |
| 477 | 501 | } |
| 478 | 502 | if (isset($settings['description'])) { |
| 479 | - $out['description'] = sanitize_text_field((string) $settings['description']); | |
| 503 | + $out['description'] = Pattern_Resolver::sanitize_template((string) $settings['description']); | |
| 480 | 504 | } |
| 481 | 505 | if (isset($settings['schema_type'])) { |
| 482 | 506 | $value = sanitize_text_field((string) $settings['schema_type']); |
| 483 | 507 | $out['schema_type'] = in_array($value, self::ALLOWED_SCHEMA_TYPES, true) ? $value : 'WebPage'; |