|
@@ -695,8 +695,40 @@ |
|
695
|
695
|
// Per-plugin sync |
|
696
|
696
|
// ------------------------------------------------------------------ |
|
697
|
697
|
|
|
698
|
698
|
/** |
|
699
|
+ * Whether third-party SEO storage may be written at all. |
|
700
|
+ * |
|
701
|
+ * The site owner's consent switch. Checked once per plugin dispatch rather |
|
702
|
+ * than per field so a sync that is not permitted does no work and, more |
|
703
|
+ * importantly, leaves no half-written row behind. |
|
704
|
+ * |
|
705
|
+ * @return bool |
|
706
|
+ */ |
|
707
|
+ private function third_party_writes_allowed() { |
|
708
|
+ return class_exists('Metasync_Seo_Backup') && Metasync_Seo_Backup::is_enabled(); |
|
709
|
+ } |
|
710
|
+ |
|
711
|
+ /** |
|
712
|
+ * Write one third-party post-meta field, preserving what it held before. |
|
713
|
+ * |
|
714
|
+ * This bridge runs on `updated_post_meta`, which means it fires from inside |
|
715
|
+ * an OTTO sync's `_metasync_otto_*` write — before the same sync reaches its |
|
716
|
+ * own direct Rank Math / Yoast writes further down. It is therefore usually |
|
717
|
+ * the first code to touch the customer's value, and the backup it takes here |
|
718
|
+ * is the one that holds the true original. |
|
719
|
+ * |
|
720
|
+ * @param int $post_id Post ID. |
|
721
|
+ * @param string $key Third-party meta key. |
|
722
|
+ * @param mixed $value Value to write. |
|
723
|
+ * @return bool True when the write happened. |
|
724
|
+ */ |
|
725
|
+ private function write_post_field($post_id, $key, $value) { |
|
726
|
+ return class_exists('Metasync_Seo_Backup') |
|
727
|
+ && Metasync_Seo_Backup::write_post_meta($post_id, $key, $value); |
|
728
|
+ } |
|
729
|
+ |
|
730
|
+ /** |
|
699
|
731
|
* Mirror canonical data into Yoast post meta and indexable cache. |
|
700
|
732
|
* |
|
701
|
733
|
* @param int $post_id Post ID. |
|
702
|
734
|
* @param array $data Canonical key/value pairs. |
|
@@ -714,32 +746,34 @@ |
|
714
|
746
|
* Counting them would make the result unconditionally true, |
|
715
|
747
|
* which is the bug this return contract exists to fix. |
|
716
|
748
|
*/ |
|
717
|
749
|
private function sync_yoast($post_id, array $data) { |
|
750
|
+ if (!$this->third_party_writes_allowed()) { |
|
751
|
+ return false; |
|
752
|
+ } |
|
753
|
+ |
|
718
|
754
|
$wrote = false; |
|
719
|
755
|
|
|
720
|
756
|
// title |
|
721
|
757
|
if (!empty($data['title'])) { |
|
722
|
|
- update_post_meta($post_id, '_yoast_wpseo_title', (string) $data['title']); |
|
723
|
|
- $wrote = true; |
|
758
|
+ $wrote = $this->write_post_field($post_id, '_yoast_wpseo_title', (string) $data['title']); |
|
724
|
759
|
} |
|
725
|
760
|
|
|
726
|
761
|
// description -- strip newlines first |
|
727
|
762
|
if (!empty($data['desc'])) { |
|
728
|
763
|
$desc = str_replace(["\n", "\r", "\t"], ' ', $data['desc']); |
|
729
|
|
- update_post_meta($post_id, '_yoast_wpseo_metadesc', $desc); |
|
730
|
|
- $wrote = true; |
|
764
|
+ $wrote = $this->write_post_field($post_id, '_yoast_wpseo_metadesc', $desc) || $wrote; |
|
731
|
765
|
} |
|
732
|
766
|
|
|
733
|
767
|
// noindex: '0'=default, '1'=noindex, '2'=index |
|
734
|
768
|
if (array_key_exists('noindex', $data)) { |
|
735
|
769
|
$val = $data['noindex'] ? '1' : '2'; |
|
736
|
|
- update_post_meta($post_id, '_yoast_wpseo_meta-robots-noindex', $val); |
|
770
|
+ $this->write_post_field($post_id, '_yoast_wpseo_meta-robots-noindex', $val); |
|
737
|
771
|
} |
|
738
|
772
|
|
|
739
|
773
|
// nofollow: '0'=follow, '1'=nofollow |
|
740
|
774
|
if (array_key_exists('nofollow', $data)) { |
|
741
|
|
- update_post_meta($post_id, '_yoast_wpseo_meta-robots-nofollow', $data['nofollow'] ? '1' : '0'); |
|
775
|
+ $this->write_post_field($post_id, '_yoast_wpseo_meta-robots-nofollow', $data['nofollow'] ? '1' : '0'); |
|
742
|
776
|
} |
|
743
|
777
|
|
|
744
|
778
|
// advanced robots: comma-separated NO spaces |
|
745
|
779
|
$adv = []; |
|
@@ -754,46 +788,38 @@ |
|
754
|
788
|
} |
|
755
|
789
|
// Written unconditionally so clearing the last directive clears the |
|
756
|
790
|
// field. Like the other robots writes it carries no canonical value, |
|
757
|
791
|
// so it does not make this a successful sync. |
|
758
|
|
- update_post_meta($post_id, '_yoast_wpseo_meta-robots-adv', implode(',', $adv)); |
|
792
|
+ $this->write_post_field($post_id, '_yoast_wpseo_meta-robots-adv', implode(',', $adv)); |
|
759
|
793
|
|
|
760
|
794
|
// OG |
|
761
|
795
|
if (!empty($data['og_title'])) { |
|
762
|
|
- update_post_meta($post_id, '_yoast_wpseo_opengraph-title', $data['og_title']); |
|
763
|
|
- $wrote = true; |
|
796
|
+ $wrote = $this->write_post_field($post_id, '_yoast_wpseo_opengraph-title', $data['og_title']) || $wrote; |
|
764
|
797
|
} |
|
765
|
798
|
if (!empty($data['og_desc'])) { |
|
766
|
|
- update_post_meta($post_id, '_yoast_wpseo_opengraph-description', $data['og_desc']); |
|
767
|
|
- $wrote = true; |
|
799
|
+ $wrote = $this->write_post_field($post_id, '_yoast_wpseo_opengraph-description', $data['og_desc']) || $wrote; |
|
768
|
800
|
} |
|
769
|
801
|
if (!empty($data['og_image'])) { |
|
770
|
|
- update_post_meta($post_id, '_yoast_wpseo_opengraph-image', esc_url_raw($data['og_image'])); |
|
771
|
|
- $wrote = true; |
|
802
|
+ $wrote = $this->write_post_field($post_id, '_yoast_wpseo_opengraph-image', esc_url_raw($data['og_image'])) || $wrote; |
|
772
|
803
|
} |
|
773
|
804
|
|
|
774
|
805
|
// Twitter |
|
775
|
806
|
if (!empty($data['twitter_title'])) { |
|
776
|
|
- update_post_meta($post_id, '_yoast_wpseo_twitter-title', $data['twitter_title']); |
|
777
|
|
- $wrote = true; |
|
807
|
+ $wrote = $this->write_post_field($post_id, '_yoast_wpseo_twitter-title', $data['twitter_title']) || $wrote; |
|
778
|
808
|
} |
|
779
|
809
|
if (!empty($data['twitter_desc'])) { |
|
780
|
|
- update_post_meta($post_id, '_yoast_wpseo_twitter-description', $data['twitter_desc']); |
|
781
|
|
- $wrote = true; |
|
810
|
+ $wrote = $this->write_post_field($post_id, '_yoast_wpseo_twitter-description', $data['twitter_desc']) || $wrote; |
|
782
|
811
|
} |
|
783
|
812
|
|
|
784
|
813
|
// Canonical, focus keyword, breadcrumb |
|
785
|
814
|
if (!empty($data['canonical'])) { |
|
786
|
|
- update_post_meta($post_id, '_yoast_wpseo_canonical', esc_url_raw($data['canonical'])); |
|
787
|
|
- $wrote = true; |
|
815
|
+ $wrote = $this->write_post_field($post_id, '_yoast_wpseo_canonical', esc_url_raw($data['canonical'])) || $wrote; |
|
788
|
816
|
} |
|
789
|
817
|
if (!empty($data['focus_keyword'])) { |
|
790
|
|
- update_post_meta($post_id, '_yoast_wpseo_focuskw', $data['focus_keyword']); |
|
791
|
|
- $wrote = true; |
|
818
|
+ $wrote = $this->write_post_field($post_id, '_yoast_wpseo_focuskw', $data['focus_keyword']) || $wrote; |
|
792
|
819
|
} |
|
793
|
820
|
if (!empty($data['breadcrumb_title'])) { |
|
794
|
|
- update_post_meta($post_id, '_yoast_wpseo_bctitle', $data['breadcrumb_title']); |
|
795
|
|
- $wrote = true; |
|
821
|
+ $wrote = $this->write_post_field($post_id, '_yoast_wpseo_bctitle', $data['breadcrumb_title']) || $wrote; |
|
796
|
822
|
} |
|
797
|
823
|
|
|
798
|
824
|
// Update wp_yoast_indexable cache row for immediate effect |
|
799
|
825
|
global $wpdb; |
|
@@ -856,8 +882,31 @@ |
|
856
|
882
|
"SELECT id FROM {$indexable_table} WHERE object_id = %d AND object_type = 'post'", |
|
857
|
883
|
$post_id |
|
858
|
884
|
)); |
|
859
|
885
|
|
|
886
|
+ // This probe decides two things at once: the value of the write-once |
|
887
|
+ // row_existed marker, and whether the write below is an UPDATE or an |
|
888
|
+ // INSERT. A failed probe answers "no row" for a row that is really |
|
889
|
+ // there, which commits a '0' marker telling a restore to delete the |
|
890
|
+ // customer's row and fires an INSERT against a row that already |
|
891
|
+ // exists. Neither is correctable afterwards, so an unreadable probe |
|
892
|
+ // has to abandon the indexable write entirely. |
|
893
|
+ // |
|
894
|
+ // The post meta above has already landed, so the receipt still |
|
895
|
+ // reports it. Only the indexable is skipped. |
|
896
|
+ if (!Metasync_Seo_Backup::db_read_succeeded()) { |
|
897
|
+ return $wrote; |
|
898
|
+ } |
|
899
|
+ |
|
900
|
+ // No original saved means no write, exactly as on the AIOSEO table. |
|
901
|
+ // This row is what Yoast actually renders from, so a restore that |
|
902
|
+ // cannot reach it puts the meta back and leaves the customer's live |
|
903
|
+ // pages still showing OTTO's values. |
|
904
|
+ $backups_created = []; |
|
905
|
+ if (!$this->backup_yoast_indexable_columns($post_id, $indexable_table, $updates, (bool) $row_exists, $backups_created)) { |
|
906
|
+ return $wrote; |
|
907
|
+ } |
|
908
|
+ |
|
860
|
909
|
if ($row_exists) { |
|
861
|
910
|
$wpdb->update( |
|
862
|
911
|
$indexable_table, |
|
863
|
912
|
$updates, |
|
@@ -879,9 +928,28 @@ |
|
879
|
928
|
'is_cornerstone' => 0, |
|
880
|
929
|
'created_at' => current_time('mysql'), |
|
881
|
930
|
'updated_at' => current_time('mysql'), |
|
882
|
931
|
], $updates); |
|
883
|
|
- $wpdb->insert($indexable_table, $insert); |
|
932
|
+ |
|
933
|
+ // The row_existed='0' marker was recorded before this insert, |
|
934
|
+ // because a marker that will not save has to be able to veto the |
|
935
|
+ // write. A failed insert means no row of ours exists, and a |
|
936
|
+ // marker left saying otherwise would let a restore delete a row |
|
937
|
+ // Yoast or the customer creates afterwards. The per-column |
|
938
|
+ // backups recorded beside it go too: they are write-once, so a |
|
939
|
+ // column captured as NULL because there was no row would stay |
|
940
|
+ // NULL for good and blank a real value the customer later puts |
|
941
|
+ // in that row. |
|
942
|
+ // |
|
943
|
+ // The post meta above has already been written, so the return |
|
944
|
+ // still reports what landed. Claiming nothing was written would |
|
945
|
+ // tell the conflict handler this post is unsynced to Yoast while |
|
946
|
+ // Yoast's own meta holds our values, and it would hand tag |
|
947
|
+ // ownership to the wrong plugin. |
|
948
|
+ if ($wpdb->insert($indexable_table, $insert) === false) { |
|
949
|
+ Metasync_Seo_Backup::discard_backups('post', $post_id, $backups_created); |
|
950
|
+ return $wrote; |
|
951
|
+ } |
|
884
|
952
|
} |
|
885
|
953
|
} |
|
886
|
954
|
|
|
887
|
955
|
return $wrote; |
|
@@ -887,8 +955,114 @@ |
|
887
|
955
|
return $wrote; |
|
888
|
956
|
} |
|
889
|
957
|
|
|
890
|
958
|
/** |
|
959
|
+ * Preserve the wp_yoast_indexable columns this sync is about to overwrite. |
|
960
|
+ * |
|
961
|
+ * Yoast serves the frontend from this table, not from post meta, so the |
|
962
|
+ * meta backups taken by write_post_field() do not by themselves make the |
|
963
|
+ * change reversible. Same shape as backup_aioseo_columns(): |
|
964
|
+ * |
|
965
|
+ * - one backup per column being written, so a restore can put the originals |
|
966
|
+ * back and leave every other column of the user's row alone; |
|
967
|
+ * - whether the row existed at all, so a restore can delete a row that only |
|
968
|
+ * exists because we created it instead of leaving an empty shell behind. |
|
969
|
+ * |
|
970
|
+ * @param int $post_id Post ID. |
|
971
|
+ * @param string $table Fully prefixed indexable table name. |
|
972
|
+ * @param array $updates Columns and values about to be written. |
|
973
|
+ * @param bool $row_existed Whether Yoast already had a row for this post. |
|
974
|
+ * @param array $created Out-param, filled with the backup fields this |
|
975
|
+ * call created, so a failed write can withdraw |
|
976
|
+ * exactly its own rows and no one else's. |
|
977
|
+ * @return bool True when every original was preserved and the caller may write. |
|
978
|
+ */ |
|
979
|
+ private function backup_yoast_indexable_columns($post_id, $table, array $updates, $row_existed, array &$created) { |
|
980
|
+ $created = []; |
|
981
|
+ |
|
982
|
+ if (!class_exists('Metasync_Seo_Backup')) { |
|
983
|
+ return false; |
|
984
|
+ } |
|
985
|
+ |
|
986
|
+ global $wpdb; |
|
987
|
+ |
|
988
|
+ if (!Metasync_Seo_Backup::record_marker( |
|
989
|
+ 'post', |
|
990
|
+ $post_id, |
|
991
|
+ 'yoast_indexable_row_existed', |
|
992
|
+ $row_existed ? '1' : '0', |
|
993
|
+ $marker_created |
|
994
|
+ )) { |
|
995
|
+ return false; |
|
996
|
+ } |
|
997
|
+ |
|
998
|
+ if ($marker_created) { |
|
999
|
+ $created[] = 'yoast_indexable_row_existed'; |
|
1000
|
+ } |
|
1001
|
+ |
|
1002
|
+ $columns = array_keys($updates); |
|
1003
|
+ if (empty($columns)) { |
|
1004
|
+ return true; |
|
1005
|
+ } |
|
1006
|
+ |
|
1007
|
+ $current = null; |
|
1008
|
+ if ($row_existed) { |
|
1009
|
+ $select = '`' . implode('`, `', array_map('esc_sql', $columns)) . '`'; |
|
1010
|
+ $current = $wpdb->get_row( |
|
1011
|
+ $wpdb->prepare( |
|
1012
|
+ "SELECT {$select} FROM {$table} WHERE object_id = %d AND object_type = 'post'", |
|
1013
|
+ $post_id |
|
1014
|
+ ), |
|
1015
|
+ ARRAY_A |
|
1016
|
+ ); |
|
1017
|
+ |
|
1018
|
+ // The row was there a moment ago, so a null answer now is a failed |
|
1019
|
+ // read, not an empty row. Recording it as "every column was NULL" |
|
1020
|
+ // would tell a later restore to delete values it should put back, |
|
1021
|
+ // which is the exact loss this layer exists to prevent. No write is |
|
1022
|
+ // happening, so the marker recorded above has to go too — a marker |
|
1023
|
+ // left describing a write that never ran is a stale story. |
|
1024
|
+ if ($current === null || !Metasync_Seo_Backup::db_read_succeeded()) { |
|
1025
|
+ Metasync_Seo_Backup::discard_backups('post', $post_id, $created); |
|
1026
|
+ return false; |
|
1027
|
+ } |
|
1028
|
+ } |
|
1029
|
+ |
|
1030
|
+ foreach ($columns as $column) { |
|
1031
|
+ // A missing row and a NULL column are the same thing to a restore: |
|
1032
|
+ // there was no value here, so put nothing back. |
|
1033
|
+ $current_value = ($current !== null && isset($current[$column])) ? $current[$column] : null; |
|
1034
|
+ |
|
1035
|
+ $field = 'yoast_indexable_' . $column; |
|
1036
|
+ |
|
1037
|
+ // One unsaved column is enough to refuse the whole write: a |
|
1038
|
+ // half-original, half-OTTO row is something no restore can unpick. |
|
1039
|
+ // The refusal also means the caller writes nothing, so withdraw the |
|
1040
|
+ // marker and the columns recorded so far — the same rule as the |
|
1041
|
+ // failed-insert path in sync_yoast(). Left behind, a row_existed='0' |
|
1042
|
+ // marker would let a restore delete a row the customer creates |
|
1043
|
+ // later, and a NULL column backup would blank a real value in it. |
|
1044
|
+ if (!Metasync_Seo_Backup::backup_before_overwrite( |
|
1045
|
+ 'post', |
|
1046
|
+ $post_id, |
|
1047
|
+ $field, |
|
1048
|
+ $updates[$column], |
|
1049
|
+ $current_value, |
|
1050
|
+ $column_created |
|
1051
|
+ )) { |
|
1052
|
+ Metasync_Seo_Backup::discard_backups('post', $post_id, $created); |
|
1053
|
+ return false; |
|
1054
|
+ } |
|
1055
|
+ |
|
1056
|
+ if ($column_created) { |
|
1057
|
+ $created[] = $field; |
|
1058
|
+ } |
|
1059
|
+ } |
|
1060
|
+ |
|
1061
|
+ return true; |
|
1062
|
+ } |
|
1063
|
+ |
|
1064
|
+ /** |
|
891
|
1065
|
* Mirror canonical data into Rank Math post meta. |
|
892
|
1066
|
* |
|
893
|
1067
|
* @param int $post_id Post ID. |
|
894
|
1068
|
* @param array $data Canonical key/value pairs. |
|
@@ -896,18 +1070,20 @@ |
|
896
|
1070
|
* false when the payload carried nothing to write. Robots |
|
897
|
1071
|
* directives are excluded for the reason given on sync_yoast(). |
|
898
|
1072
|
*/ |
|
899
|
1073
|
private function sync_rankmath($post_id, array $data) { |
|
1074
|
+ if (!$this->third_party_writes_allowed()) { |
|
1075
|
+ return false; |
|
1076
|
+ } |
|
1077
|
+ |
|
900
|
1078
|
$wrote = false; |
|
901
|
1079
|
|
|
902
|
1080
|
// title, desc |
|
903
|
1081
|
if (!empty($data['title'])) { |
|
904
|
|
- update_post_meta($post_id, 'rank_math_title', $data['title']); |
|
905
|
|
- $wrote = true; |
|
1082
|
+ $wrote = $this->write_post_field($post_id, 'rank_math_title', $data['title']); |
|
906
|
1083
|
} |
|
907
|
1084
|
if (!empty($data['desc'])) { |
|
908
|
|
- update_post_meta($post_id, 'rank_math_description', $data['desc']); |
|
909
|
|
- $wrote = true; |
|
1085
|
+ $wrote = $this->write_post_field($post_id, 'rank_math_description', $data['desc']) || $wrote; |
|
910
|
1086
|
} |
|
911
|
1087
|
|
|
912
|
1088
|
// robots: PHP indexed array |
|
913
|
1089
|
if (array_key_exists('noindex', $data) || array_key_exists('nofollow', $data)) { |
|
@@ -919,9 +1095,9 @@ |
|
919
|
1095
|
} |
|
920
|
1096
|
if (array_key_exists('nofollow', $data)) { |
|
921
|
1097
|
$robots[] = $data['nofollow'] ? 'nofollow' : 'follow'; |
|
922
|
1098
|
} |
|
923
|
|
- update_post_meta($post_id, 'rank_math_robots', array_values(array_unique($robots))); |
|
1099
|
+ $this->write_post_field($post_id, 'rank_math_robots', array_values(array_unique($robots))); |
|
924
|
1100
|
} |
|
925
|
1101
|
|
|
926
|
1102
|
// Advanced robots: max-* go into rank_math_advanced_robots |
|
927
|
1103
|
$adv_keys = ['max_snippet', 'max_image_preview', 'max_video_preview']; |
|
@@ -949,9 +1125,9 @@ |
|
949
|
1125
|
$val = (int) $data['max_video_preview']; |
|
950
|
1126
|
$adv['max-video-preview'] = (string) $val; |
|
951
|
1127
|
} |
|
952
|
1128
|
if (!empty($adv)) { |
|
953
|
|
- update_post_meta($post_id, 'rank_math_advanced_robots', $adv); |
|
1129
|
+ $this->write_post_field($post_id, 'rank_math_advanced_robots', $adv); |
|
954
|
1130
|
} |
|
955
|
1131
|
} |
|
956
|
1132
|
|
|
957
|
1133
|
// Sync noarchive/nosnippet/noimageindex into rank_math_robots |
|
@@ -966,58 +1142,49 @@ |
|
966
|
1142
|
if (!empty($data[$dir])) { |
|
967
|
1143
|
$robots[] = $dir; |
|
968
|
1144
|
} |
|
969
|
1145
|
} |
|
970
|
|
- update_post_meta($post_id, 'rank_math_robots', array_values(array_unique($robots))); |
|
1146
|
+ $this->write_post_field($post_id, 'rank_math_robots', array_values(array_unique($robots))); |
|
971
|
1147
|
} |
|
972
|
1148
|
|
|
973
|
1149
|
// OG |
|
974
|
1150
|
if (!empty($data['og_title'])) { |
|
975
|
|
- update_post_meta($post_id, 'rank_math_facebook_title', $data['og_title']); |
|
976
|
|
- $wrote = true; |
|
1151
|
+ $wrote = $this->write_post_field($post_id, 'rank_math_facebook_title', $data['og_title']) || $wrote; |
|
977
|
1152
|
} |
|
978
|
1153
|
if (!empty($data['og_desc'])) { |
|
979
|
|
- update_post_meta($post_id, 'rank_math_facebook_description', $data['og_desc']); |
|
980
|
|
- $wrote = true; |
|
1154
|
+ $wrote = $this->write_post_field($post_id, 'rank_math_facebook_description', $data['og_desc']) || $wrote; |
|
981
|
1155
|
} |
|
982
|
1156
|
if (!empty($data['og_image'])) { |
|
983
|
|
- update_post_meta($post_id, 'rank_math_facebook_image', esc_url_raw($data['og_image'])); |
|
1157
|
+ $wrote = $this->write_post_field($post_id, 'rank_math_facebook_image', esc_url_raw($data['og_image'])) || $wrote; |
|
984
|
1158
|
$img_id = attachment_url_to_postid($data['og_image']); |
|
985
|
1159
|
if ($img_id) { |
|
986
|
|
- update_post_meta($post_id, 'rank_math_facebook_image_id', $img_id); |
|
1160
|
+ $wrote = $this->write_post_field($post_id, 'rank_math_facebook_image_id', $img_id) || $wrote; |
|
987
|
1161
|
} |
|
988
|
|
- $wrote = true; |
|
989
|
1162
|
} |
|
990
|
1163
|
|
|
991
|
1164
|
// Twitter |
|
992
|
1165
|
if (!empty($data['twitter_title'])) { |
|
993
|
|
- update_post_meta($post_id, 'rank_math_twitter_title', $data['twitter_title']); |
|
994
|
|
- $wrote = true; |
|
1166
|
+ $wrote = $this->write_post_field($post_id, 'rank_math_twitter_title', $data['twitter_title']) || $wrote; |
|
995
|
1167
|
} |
|
996
|
1168
|
if (!empty($data['twitter_desc'])) { |
|
997
|
|
- update_post_meta($post_id, 'rank_math_twitter_description', $data['twitter_desc']); |
|
998
|
|
- $wrote = true; |
|
1169
|
+ $wrote = $this->write_post_field($post_id, 'rank_math_twitter_description', $data['twitter_desc']) || $wrote; |
|
999
|
1170
|
} |
|
1000
|
1171
|
if (!empty($data['twitter_card'])) { |
|
1001
|
1172
|
$valid_cards = ['summary', 'summary_large_image', 'app', 'player']; |
|
1002
|
1173
|
if (in_array($data['twitter_card'], $valid_cards, true)) { |
|
1003
|
|
- update_post_meta($post_id, 'rank_math_twitter_card_type', $data['twitter_card']); |
|
1004
|
|
- $wrote = true; |
|
1174
|
+ $wrote = $this->write_post_field($post_id, 'rank_math_twitter_card_type', $data['twitter_card']) || $wrote; |
|
1005
|
1175
|
} |
|
1006
|
1176
|
} |
|
1007
|
1177
|
|
|
1008
|
1178
|
// Canonical, focus keyword, breadcrumb |
|
1009
|
1179
|
if (!empty($data['canonical'])) { |
|
1010
|
|
- update_post_meta($post_id, 'rank_math_canonical_url', esc_url_raw($data['canonical'])); |
|
1011
|
|
- $wrote = true; |
|
1180
|
+ $wrote = $this->write_post_field($post_id, 'rank_math_canonical_url', esc_url_raw($data['canonical'])) || $wrote; |
|
1012
|
1181
|
} |
|
1013
|
1182
|
if (!empty($data['focus_keyword'])) { |
|
1014
|
|
- update_post_meta($post_id, 'rank_math_focus_keyword', $data['focus_keyword']); |
|
1015
|
|
- $wrote = true; |
|
1183
|
+ $wrote = $this->write_post_field($post_id, 'rank_math_focus_keyword', $data['focus_keyword']) || $wrote; |
|
1016
|
1184
|
} |
|
1017
|
1185
|
if (!empty($data['breadcrumb_title'])) { |
|
1018
|
|
- update_post_meta($post_id, 'rank_math_breadcrumb_title', $data['breadcrumb_title']); |
|
1019
|
|
- $wrote = true; |
|
1186
|
+ $wrote = $this->write_post_field($post_id, 'rank_math_breadcrumb_title', $data['breadcrumb_title']) || $wrote; |
|
1020
|
1187
|
} |
|
1021
|
1188
|
|
|
1022
|
1189
|
return $wrote; |
|
1023
|
1190
|
} |
|
@@ -1022,8 +1189,116 @@ |
|
1022
|
1189
|
return $wrote; |
|
1023
|
1190
|
} |
|
1024
|
1191
|
|
|
1025
|
1192
|
/** |
|
1193
|
+ * Preserve the AIOSEO columns a sync is about to overwrite. |
|
1194
|
+ * |
|
1195
|
+ * AIOSEO keeps post SEO data in its own `aioseo_posts` table rather than in |
|
1196
|
+ * post meta, so there is no meta row to save and no field a restore could |
|
1197
|
+ * delete. Two things are recorded instead, both as post meta on the post |
|
1198
|
+ * itself: |
|
1199
|
+ * |
|
1200
|
+ * - one backup per column being written, so a restore can put the original |
|
1201
|
+ * values back and leave every other column of the user's row alone; |
|
1202
|
+ * - whether the row existed at all, so a restore can delete a row that only |
|
1203
|
+ * exists because we created it instead of leaving an empty shell behind. |
|
1204
|
+ * |
|
1205
|
+ * @param int $post_id Post ID. |
|
1206
|
+ * @param string $table Fully prefixed AIOSEO table name. |
|
1207
|
+ * @param array $row Columns and values about to be written. |
|
1208
|
+ * @param bool $row_existed Whether AIOSEO already had a row for this post. |
|
1209
|
+ * @param array $created Out-param, filled with the backup fields this |
|
1210
|
+ * call created, so a failed write can withdraw |
|
1211
|
+ * exactly its own rows and no one else's. |
|
1212
|
+ * @return bool True when every original was preserved and the caller may write. |
|
1213
|
+ */ |
|
1214
|
+ private function backup_aioseo_columns($post_id, $table, array $row, $row_existed, array &$created) { |
|
1215
|
+ $created = []; |
|
1216
|
+ |
|
1217
|
+ if (!class_exists('Metasync_Seo_Backup')) { |
|
1218
|
+ return false; |
|
1219
|
+ } |
|
1220
|
+ |
|
1221
|
+ global $wpdb; |
|
1222
|
+ |
|
1223
|
+ // Whether the row pre-existed is what a restore uses to choose between |
|
1224
|
+ // putting the original columns back and deleting a row that only exists |
|
1225
|
+ // because we made it. A marker that will not record is as disqualifying |
|
1226
|
+ // as a column that will not. |
|
1227
|
+ if (!Metasync_Seo_Backup::record_marker( |
|
1228
|
+ 'post', |
|
1229
|
+ $post_id, |
|
1230
|
+ 'aioseo_row_existed', |
|
1231
|
+ $row_existed ? '1' : '0', |
|
1232
|
+ $marker_created |
|
1233
|
+ )) { |
|
1234
|
+ return false; |
|
1235
|
+ } |
|
1236
|
+ |
|
1237
|
+ if ($marker_created) { |
|
1238
|
+ $created[] = 'aioseo_row_existed'; |
|
1239
|
+ } |
|
1240
|
+ |
|
1241
|
+ $columns = array_diff(array_keys($row), ['updated', 'created', 'post_id']); |
|
1242
|
+ if (empty($columns)) { |
|
1243
|
+ return true; |
|
1244
|
+ } |
|
1245
|
+ |
|
1246
|
+ $current = null; |
|
1247
|
+ if ($row_existed) { |
|
1248
|
+ $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), |
|
1251
|
+ ARRAY_A |
|
1252
|
+ ); |
|
1253
|
+ |
|
1254
|
+ // The row was there a moment ago, so a null answer now is a failed |
|
1255
|
+ // read, not an empty row. Recording it as "every column was NULL" |
|
1256
|
+ // would tell a later restore to delete values it should put back, |
|
1257
|
+ // which is the exact loss this layer exists to prevent. No write is |
|
1258
|
+ // happening, so the marker recorded above has to go too — a marker |
|
1259
|
+ // left describing a write that never ran is a stale story. |
|
1260
|
+ if ($current === null || !Metasync_Seo_Backup::db_read_succeeded()) { |
|
1261
|
+ Metasync_Seo_Backup::discard_backups('post', $post_id, $created); |
|
1262
|
+ return false; |
|
1263
|
+ } |
|
1264
|
+ } |
|
1265
|
+ |
|
1266
|
+ foreach ($columns as $column) { |
|
1267
|
+ // A missing row and a NULL column are the same thing to a restore: |
|
1268
|
+ // there was no value here, so put nothing back. |
|
1269
|
+ $current_value = ($current !== null && isset($current[$column])) ? $current[$column] : null; |
|
1270
|
+ |
|
1271
|
+ $field = 'aioseo_' . $column; |
|
1272
|
+ |
|
1273
|
+ // One unsaved column is enough to refuse the whole write: a half-original, |
|
1274
|
+ // half-OTTO row is something no restore can unpick. The refusal also |
|
1275
|
+ // means the caller writes nothing, so withdraw the marker and the |
|
1276
|
+ // columns recorded so far — the same rule as the failed-insert path |
|
1277
|
+ // in sync_aioseo(). Left behind, a row_existed='0' marker would let |
|
1278
|
+ // a restore delete a row the customer creates later, and a NULL |
|
1279
|
+ // column backup would blank a real value in it. |
|
1280
|
+ if (!Metasync_Seo_Backup::backup_before_overwrite( |
|
1281
|
+ 'post', |
|
1282
|
+ $post_id, |
|
1283
|
+ $field, |
|
1284
|
+ $row[$column], |
|
1285
|
+ $current_value, |
|
1286
|
+ $column_created |
|
1287
|
+ )) { |
|
1288
|
+ Metasync_Seo_Backup::discard_backups('post', $post_id, $created); |
|
1289
|
+ return false; |
|
1290
|
+ } |
|
1291
|
+ |
|
1292
|
+ if ($column_created) { |
|
1293
|
+ $created[] = $field; |
|
1294
|
+ } |
|
1295
|
+ } |
|
1296
|
+ |
|
1297
|
+ return true; |
|
1298
|
+ } |
|
1299
|
+ |
|
1300
|
+ /** |
|
1026
|
1301
|
* Mirror canonical data into the AIOSEO wp_aioseo_posts custom table. |
|
1027
|
1302
|
* |
|
1028
|
1303
|
* @param int $post_id Post ID. |
|
1029
|
1304
|
* @param array $data Canonical key/value pairs. |
|
@@ -1035,8 +1310,12 @@ |
|
1035
|
1310
|
*/ |
|
1036
|
1311
|
private function sync_aioseo($post_id, array $data) { |
|
1037
|
1312
|
global $wpdb; |
|
1038
|
1313
|
|
|
1314
|
+ if (!$this->third_party_writes_allowed()) { |
|
1315
|
+ return false; |
|
1316
|
+ } |
|
1317
|
+ |
|
1039
|
1318
|
$table = $wpdb->prefix . 'aioseo_posts'; |
|
1040
|
1319
|
|
|
1041
|
1320
|
// Bail if the AIOSEO post table does not exist (plugin not initialised). |
|
1042
|
1321
|
$table_exists = $wpdb->get_var($wpdb->prepare('SHOW TABLES LIKE %s', $table)); |
|
@@ -1152,12 +1431,33 @@ |
|
1152
|
1431
|
"SELECT id FROM {$table} WHERE post_id = %d", |
|
1153
|
1432
|
$post_id |
|
1154
|
1433
|
)); |
|
1155
|
1434
|
|
|
1435
|
+ // An unreadable probe cannot be treated as "no row". It would commit a |
|
1436
|
+ // write-once row_existed='0' for a row AIOSEO really has -- which a |
|
1437
|
+ // restore reads as licence to delete it -- and send an INSERT at a row |
|
1438
|
+ // that already exists. Leave AIOSEO's row alone and let the next sync |
|
1439
|
+ // record the truth. |
|
1440
|
+ if (!Metasync_Seo_Backup::db_read_succeeded()) { |
|
1441
|
+ return false; |
|
1442
|
+ } |
|
1443
|
+ |
|
1444
|
+ // No original saved means no write. Overwriting anyway is the data loss |
|
1445
|
+ // this whole layer exists to prevent. |
|
1156
|
1446
|
if ($existing_id) { |
|
1447
|
+ $backups_created = []; |
|
1448
|
+ if (!$this->backup_aioseo_columns($post_id, $table, $row, true, $backups_created)) { |
|
1449
|
+ return false; |
|
1450
|
+ } |
|
1451
|
+ |
|
1157
|
1452
|
return ($wpdb->update($table, $row, ['post_id' => $post_id]) !== false) && $wrote; |
|
1158
|
1453
|
} |
|
1159
|
1454
|
|
|
1455
|
+ $backups_created = []; |
|
1456
|
+ if (!$this->backup_aioseo_columns($post_id, $table, $row, false, $backups_created)) { |
|
1457
|
+ return false; |
|
1458
|
+ } |
|
1459
|
+ |
|
1160
|
1460
|
// New row -- must include all NOT NULL columns with no defaults |
|
1161
|
1461
|
$row['post_id'] = $post_id; |
|
1162
|
1462
|
$row['created'] = current_time('mysql'); |
|
1163
|
1463
|
$robot_defaults = [ |
|
@@ -1171,9 +1471,23 @@ |
|
1171
|
1471
|
'robots_notranslate' => 0, |
|
1172
|
1472
|
]; |
|
1173
|
1473
|
$row = array_merge($robot_defaults, $row); |
|
1174
|
1474
|
|
|
1175
|
|
- return ($wpdb->insert($table, $row) !== false) && $wrote; |
|
1475
|
+ $inserted = $wpdb->insert($table, $row); |
|
1476
|
+ |
|
1477
|
+ // The row_existed='0' marker was recorded before the insert, because a |
|
1478
|
+ // marker that will not save has to be able to veto the write. If the |
|
1479
|
+ // insert then failed there is no row of ours, and leaving the marker |
|
1480
|
+ // behind would let a restore delete a row the customer creates later. |
|
1481
|
+ // The per-column backups beside it go too, or a column captured as NULL |
|
1482
|
+ // for a row that never existed would blank a real value the customer |
|
1483
|
+ // later puts in one. |
|
1484
|
+ if ($inserted === false) { |
|
1485
|
+ Metasync_Seo_Backup::discard_backups('post', $post_id, $backups_created); |
|
1486
|
+ return false; |
|
1487
|
+ } |
|
1488
|
+ |
|
1489
|
+ return $wrote; |
|
1176
|
1490
|
} |
|
1177
|
1491
|
|
|
1178
|
1492
|
// ------------------------------------------------------------------ |
|
1179
|
1493
|
// Two-way sync: sidebar JSON ↔ legacy meta boxes |