| @@ -61,8 +61,9 @@ | ||
| 61 | 61 | '_metasync_seo_title', |
| 62 | 62 | '_metasync_seo_desc', |
| 63 | 63 | '_metasync_metatitle', |
| 64 | 64 | '_metasync_metadesc', |
| 65 | + '_metasync_metakeywords', | |
| 65 | 66 | '_metasync_robots_index', |
| 66 | 67 | '_metasync_robots_advanced', |
| 67 | 68 | '_metasync_og_title', |
| 68 | 69 | '_metasync_og_description', |
| @@ -80,8 +81,9 @@ | ||
| 80 | 81 | '_metasync_otto_og_description', |
| 81 | 82 | '_metasync_otto_twitter_title', |
| 82 | 83 | '_metasync_otto_twitter_description', |
| 83 | 84 | '_metasync_otto_keywords', |
| 85 | + '_metasync_otto_canonical', | |
| 84 | 86 | ]; |
| 85 | 87 | |
| 86 | 88 | /** |
| 87 | 89 | * Internal OTTO meta keys mapped to the OTTO Persistence setting that |
| @@ -102,8 +104,9 @@ | ||
| 102 | 104 | const OTTO_PERSISTENCE_KEYS = [ |
| 103 | 105 | '_metasync_otto_title' => 'meta_title', |
| 104 | 106 | '_metasync_otto_description' => 'meta_description', |
| 105 | 107 | '_metasync_otto_keywords' => 'meta_keywords', |
| 108 | + '_metasync_metakeywords' => 'meta_keywords', | |
| 106 | 109 | '_metasync_otto_og_title' => 'og_title', |
| 107 | 110 | '_metasync_otto_og_description' => 'og_description', |
| 108 | 111 | '_metasync_otto_twitter_title' => 'twitter_title', |
| 109 | 112 | '_metasync_otto_twitter_description' => 'twitter_description', |
| @@ -253,8 +256,9 @@ | ||
| 253 | 256 | '_metasync_otto_og_description' => 'og_desc', |
| 254 | 257 | '_metasync_otto_twitter_title' => 'twitter_title', |
| 255 | 258 | '_metasync_otto_twitter_description' => 'twitter_desc', |
| 256 | 259 | '_metasync_otto_keywords' => 'focus_keyword', |
| 260 | + '_metasync_metakeywords' => 'focus_keyword', | |
| 257 | 261 | // Legacy meta box keys → rebuild JSON |
| 258 | 262 | 'metasync_common_robots' => '_legacy_robots_to_json', |
| 259 | 263 | 'metasync_advance_robots' => '_legacy_robots_to_json', |
| 260 | 264 | ]; |
| @@ -559,9 +563,13 @@ | ||
| 559 | 563 | // Canonical, focus keyword, breadcrumb |
| 560 | 564 | // Canonical is validated at the source so a corrupted value ("Array") |
| 561 | 565 | // never propagates into Yoast/RankMath/AIOSEO storage. |
| 562 | 566 | $data['canonical'] = Metasync_Canonical_Sanitizer::sanitize($get('_metasync_canonical_url')); |
| 563 | - $data['focus_keyword'] = $first('_metasync_focus_keyword', '_metasync_otto_keywords'); | |
| 567 | + // The keyword takes its order from the resolver — customer value, then | |
| 568 | + // OTTO's volatile tier, then OTTO's persisted tier — so the persistence | |
| 569 | + // gating in $first() reaches the persisted copy too. Previously the | |
| 570 | + // persisted copy sat on the customer key, outranking the live one. | |
| 571 | + $data['focus_keyword'] = $first(...Metasync_Seo_Precedence::keys(Metasync_Seo_Precedence::FIELD_FOCUS_KEYWORD)); | |
| 564 | 572 | $data['breadcrumb_title'] = $get('_metasync_breadcrumb_title'); |
| 565 | 573 | |
| 566 | 574 | return $this->apply_feature_flags_to_payload($data); |
| 567 | 575 | } |
| @@ -857,9 +865,9 @@ | ||
| 857 | 865 | $updates['twitter_title'] = mb_substr($data['twitter_title'], 0, 191); |
| 858 | 866 | } |
| 859 | 867 | if (!empty($data['twitter_card'])) { |
| 860 | 868 | // twitter_card column only exists in newer Yoast versions; skip if absent |
| 861 | - $col_check = $wpdb->get_var($wpdb->prepare( | |
| 869 | + $col_check = $wpdb->get_var($wpdb->prepare( // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- mirrors SEO meta into third-party plugin storage (Yoast indexables/AIOSEO tables) — no WordPress API for their schemas | |
| 862 | 870 | "SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = %s AND TABLE_NAME = %s AND COLUMN_NAME = 'twitter_card'", |
| 863 | 871 | DB_NAME, |
| 864 | 872 | $indexable_table |
| 865 | 873 | )); |
| @@ -877,10 +885,10 @@ | ||
| 877 | 885 | $updates['breadcrumb_title'] = mb_substr($data['breadcrumb_title'], 0, 191); |
| 878 | 886 | } |
| 879 | 887 | |
| 880 | 888 | if (!empty($updates)) { |
| 881 | - $row_exists = $wpdb->get_var($wpdb->prepare( | |
| 882 | - "SELECT id FROM {$indexable_table} WHERE object_id = %d AND object_type = 'post'", | |
| 889 | + $row_exists = $wpdb->get_var($wpdb->prepare( // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- mirrors SEO meta into third-party plugin storage (Yoast indexables/AIOSEO tables) — no WordPress API for their schemas | |
| 890 | + "SELECT id FROM {$wpdb->prefix}yoast_indexable WHERE object_id = %d AND object_type = 'post'", | |
| 883 | 891 | $post_id |
| 884 | 892 | )); |
| 885 | 893 | |
| 886 | 894 | // This probe decides two things at once: the value of the write-once |
| @@ -906,9 +914,9 @@ | ||
| 906 | 914 | return $wrote; |
| 907 | 915 | } |
| 908 | 916 | |
| 909 | 917 | if ($row_exists) { |
| 910 | - $wpdb->update( | |
| 918 | + $wpdb->update( // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- mirrors SEO meta into third-party plugin storage (Yoast indexables/AIOSEO tables) — no WordPress API for their schemas | |
| 911 | 919 | $indexable_table, |
| 912 | 920 | $updates, |
| 913 | 921 | ['object_id' => $post_id, 'object_type' => 'post'] |
| 914 | 922 | ); |
| @@ -944,9 +952,9 @@ | ||
| 944 | 952 | // still reports what landed. Claiming nothing was written would |
| 945 | 953 | // tell the conflict handler this post is unsynced to Yoast while |
| 946 | 954 | // Yoast's own meta holds our values, and it would hand tag |
| 947 | 955 | // ownership to the wrong plugin. |
| 948 | - if ($wpdb->insert($indexable_table, $insert) === false) { | |
| 956 | + if ($wpdb->insert($indexable_table, $insert) === false) { // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- mirrors SEO meta into third-party plugin storage (Yoast indexables/AIOSEO tables) — no WordPress API for their schemas | |
| 949 | 957 | Metasync_Seo_Backup::discard_backups('post', $post_id, $backups_created); |
| 950 | 958 | return $wrote; |
| 951 | 959 | } |
| 952 | 960 | } |
| @@ -1006,11 +1014,13 @@ | ||
| 1006 | 1014 | |
| 1007 | 1015 | $current = null; |
| 1008 | 1016 | if ($row_existed) { |
| 1009 | 1017 | $select = '`' . implode('`, `', array_map('esc_sql', $columns)) . '`'; |
| 1010 | - $current = $wpdb->get_row( | |
| 1018 | + // Column list is esc_sql()'d above; the table is a fixed literal. | |
| 1019 | + $current = $wpdb->get_row( // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- mirrors SEO meta into third-party plugin storage (Yoast indexables/AIOSEO tables) — no WordPress API for their schemas | |
| 1011 | 1020 | $wpdb->prepare( |
| 1012 | - "SELECT {$select} FROM {$table} WHERE object_id = %d AND object_type = 'post'", | |
| 1021 | + // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- column list esc_sql()'d above, table is a fixed literal | |
| 1022 | + "SELECT {$select} FROM {$wpdb->prefix}yoast_indexable WHERE object_id = %d AND object_type = 'post'", | |
| 1013 | 1023 | $post_id |
| 1014 | 1024 | ), |
| 1015 | 1025 | ARRAY_A |
| 1016 | 1026 | ); |
| @@ -1245,10 +1255,15 @@ | ||
| 1245 | 1255 | |
| 1246 | 1256 | $current = null; |
| 1247 | 1257 | if ($row_existed) { |
| 1248 | 1258 | $select = '`' . implode('`, `', array_map('esc_sql', $columns)) . '`'; |
| 1249 | - $current = $wpdb->get_row( | |
| 1250 | - $wpdb->prepare("SELECT {$select} FROM {$table} WHERE post_id = %d", $post_id), | |
| 1259 | + // Column list is esc_sql()'d above; the table is a fixed literal. | |
| 1260 | + $current = $wpdb->get_row( // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- mirrors SEO meta into third-party plugin storage (Yoast indexables/AIOSEO tables) — no WordPress API for their schemas | |
| 1261 | + $wpdb->prepare( | |
| 1262 | + // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- column list esc_sql()'d above, table is a fixed literal | |
| 1263 | + "SELECT {$select} FROM {$wpdb->prefix}aioseo_posts WHERE post_id = %d", | |
| 1264 | + $post_id | |
| 1265 | + ), | |
| 1251 | 1266 | ARRAY_A |
| 1252 | 1267 | ); |
| 1253 | 1268 | |
| 1254 | 1269 | // The row was there a moment ago, so a null answer now is a failed |
| @@ -1317,9 +1332,9 @@ | ||
| 1317 | 1332 | |
| 1318 | 1333 | $table = $wpdb->prefix . 'aioseo_posts'; |
| 1319 | 1334 | |
| 1320 | 1335 | // Bail if the AIOSEO post table does not exist (plugin not initialised). |
| 1321 | - $table_exists = $wpdb->get_var($wpdb->prepare('SHOW TABLES LIKE %s', $table)); | |
| 1336 | + $table_exists = $wpdb->get_var($wpdb->prepare('SHOW TABLES LIKE %s', $table)); // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- mirrors SEO meta into third-party plugin storage (Yoast indexables/AIOSEO tables) — no WordPress API for their schemas | |
| 1322 | 1337 | if ($table_exists !== $table) { |
| 1323 | 1338 | return false; |
| 1324 | 1339 | } |
| 1325 | 1340 | |
| @@ -1426,10 +1441,10 @@ | ||
| 1426 | 1441 | } |
| 1427 | 1442 | |
| 1428 | 1443 | $row['updated'] = current_time('mysql'); |
| 1429 | 1444 | |
| 1430 | - $existing_id = $wpdb->get_var($wpdb->prepare( | |
| 1431 | - "SELECT id FROM {$table} WHERE post_id = %d", | |
| 1445 | + $existing_id = $wpdb->get_var($wpdb->prepare( // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- mirrors SEO meta into third-party plugin storage (Yoast indexables/AIOSEO tables) — no WordPress API for their schemas | |
| 1446 | + "SELECT id FROM {$wpdb->prefix}aioseo_posts WHERE post_id = %d", | |
| 1432 | 1447 | $post_id |
| 1433 | 1448 | )); |
| 1434 | 1449 | |
| 1435 | 1450 | // An unreadable probe cannot be treated as "no row". It would commit a |
| @@ -1448,9 +1463,9 @@ | ||
| 1448 | 1463 | if (!$this->backup_aioseo_columns($post_id, $table, $row, true, $backups_created)) { |
| 1449 | 1464 | return false; |
| 1450 | 1465 | } |
| 1451 | 1466 | |
| 1452 | - return ($wpdb->update($table, $row, ['post_id' => $post_id]) !== false) && $wrote; | |
| 1467 | + return ($wpdb->update($table, $row, ['post_id' => $post_id]) !== false) && $wrote; // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- mirrors SEO meta into third-party plugin storage (Yoast indexables/AIOSEO tables) — no WordPress API for their schemas | |
| 1453 | 1468 | } |
| 1454 | 1469 | |
| 1455 | 1470 | $backups_created = []; |
| 1456 | 1471 | if (!$this->backup_aioseo_columns($post_id, $table, $row, false, $backups_created)) { |
| @@ -1471,9 +1486,9 @@ | ||
| 1471 | 1486 | 'robots_notranslate' => 0, |
| 1472 | 1487 | ]; |
| 1473 | 1488 | $row = array_merge($robot_defaults, $row); |
| 1474 | 1489 | |
| 1475 | - $inserted = $wpdb->insert($table, $row); | |
| 1490 | + $inserted = $wpdb->insert($table, $row); // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- mirrors SEO meta into third-party plugin storage (Yoast indexables/AIOSEO tables) — no WordPress API for their schemas | |
| 1476 | 1491 | |
| 1477 | 1492 | // The row_existed='0' marker was recorded before the insert, because a |
| 1478 | 1493 | // marker that will not save has to be able to veto the write. If the |
| 1479 | 1494 | // insert then failed there is no row of ours, and leaving the marker |