|
@@ -283,8 +283,16 @@ |
|
283
|
283
|
if ($this->is_yoast_active()) { |
|
284
|
284
|
$sync_instance = $this; |
|
285
|
285
|
$sync_post_id = (int) $post_id; |
|
286
|
286
|
add_action('shutdown', function() use ($sync_instance, $sync_post_id) { |
|
287
|
+ // By shutdown this request has typically seeded the OG defaults |
|
288
|
+ // memo for the post (the sync above reads it); the row write |
|
289
|
+ // has since landed, so drop the memo or the re-sync below |
|
290
|
+ // compares against the pre-save title/excerpt. |
|
291
|
+ // @phpstan-ignore-next-line function.alreadyNarrowedType |
|
292
|
+ if (method_exists('Metasync_OpenGraph', 'clear_default_og_values_memo')) { |
|
293
|
+ Metasync_OpenGraph::clear_default_og_values_memo(); |
|
294
|
+ } |
|
287
|
295
|
$sync_instance->sync_post($sync_post_id); |
|
288
|
296
|
}, 0); |
|
289
|
297
|
} |
|
290
|
298
|
return; |
|
@@ -382,9 +390,9 @@ |
|
382
|
390
|
} |
|
383
|
391
|
|
|
384
|
392
|
$all_meta = get_post_custom($post_id); |
|
385
|
393
|
|
|
386
|
|
- $get = function ($key) use ($all_meta) { |
|
394
|
+ $get = function ($key) use ($all_meta, $post_id) { |
|
387
|
395
|
if (!isset($all_meta[$key])) { |
|
388
|
396
|
return ''; |
|
389
|
397
|
} |
|
390
|
398
|
$value = is_array($all_meta[$key]) ? $all_meta[$key][0] : $all_meta[$key]; |
|
@@ -403,11 +411,38 @@ |
|
403
|
411
|
if (method_exists('Metasync_OpenGraph', 'strip_auto_draft_title') |
|
404
|
412
|
&& defined('Metasync_OpenGraph::AUTO_DRAFT_PRONE_KEYS') |
|
405
|
413
|
&& in_array($key, Metasync_OpenGraph::AUTO_DRAFT_PRONE_KEYS, true) |
|
406
|
414
|
) { |
|
407
|
|
- return Metasync_OpenGraph::strip_auto_draft_title($value); |
|
415
|
+ $value = Metasync_OpenGraph::strip_auto_draft_title($value); |
|
408
|
416
|
} |
|
409
|
417
|
|
|
418
|
+ // A social title that is a verbatim snapshot of the post title is the |
|
419
|
+ // old pre-fill, not a customization — mirroring it would hand a |
|
420
|
+ // third-party plugin a copy that a rename leaves stale (and that |
|
421
|
+ // outranks OTTO's fresh value there). Collapsed for the same reason |
|
422
|
+ // as the placeholder above; the chain in $first then falls through |
|
423
|
+ // to OTTO's staging key. |
|
424
|
+ // @phpstan-ignore-next-line function.alreadyNarrowedType |
|
425
|
+ if (method_exists('Metasync_OpenGraph', 'strip_title_snapshot') |
|
426
|
+ && defined('Metasync_OpenGraph::TITLE_DEFAULTED_KEYS') |
|
427
|
+ && in_array($key, Metasync_OpenGraph::TITLE_DEFAULTED_KEYS, true) |
|
428
|
+ ) { |
|
429
|
+ $value = Metasync_OpenGraph::strip_title_snapshot($post_id, $key, $value); |
|
430
|
+ } |
|
431
|
+ |
|
432
|
+ // A social description that is a verbatim snapshot of the resolved |
|
433
|
+ // excerpt is the old pre-fill, not a customization — mirroring it |
|
434
|
+ // would hand a third-party plugin a copy that a later excerpt or |
|
435
|
+ // content edit leaves stale. Collapsed for the same reason as the |
|
436
|
+ // title snapshot above. |
|
437
|
+ // @phpstan-ignore-next-line function.alreadyNarrowedType |
|
438
|
+ if (method_exists('Metasync_OpenGraph', 'strip_description_snapshot') |
|
439
|
+ && defined('Metasync_OpenGraph::DESCRIPTION_DEFAULTED_KEYS') |
|
440
|
+ && in_array($key, Metasync_OpenGraph::DESCRIPTION_DEFAULTED_KEYS, true) |
|
441
|
+ ) { |
|
442
|
+ $value = Metasync_OpenGraph::strip_description_snapshot($post_id, $key, $value); |
|
443
|
+ } |
|
444
|
+ |
|
410
|
445
|
return $value; |
|
411
|
446
|
}; |
|
412
|
447
|
|
|
413
|
448
|
$data = []; |
|
@@ -660,35 +695,85 @@ |
|
660
|
695
|
// Per-plugin sync |
|
661
|
696
|
// ------------------------------------------------------------------ |
|
662
|
697
|
|
|
663
|
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
|
+ /** |
|
664
|
731
|
* Mirror canonical data into Yoast post meta and indexable cache. |
|
665
|
732
|
* |
|
666
|
733
|
* @param int $post_id Post ID. |
|
667
|
734
|
* @param array $data Canonical key/value pairs. |
|
668
|
|
- * @return bool True once dispatch completes. |
|
735
|
+ * @return bool True when at least one value-bearing field was mirrored, |
|
736
|
+ * false when the payload carried nothing to write. The caller |
|
737
|
+ * stamps `_metasync_plugin_sync_ts` from this, so it must |
|
738
|
+ * describe what was written and not merely that dispatch was |
|
739
|
+ * reached. |
|
740
|
+ * |
|
741
|
+ * Robots directives are excluded from that answer on purpose. |
|
742
|
+ * collect_post_data() synthesises noindex/nofollow on every |
|
743
|
+ * payload — absent any stored value they resolve to plain |
|
744
|
+ * index/follow — so they are written on every sync and say |
|
745
|
+ * nothing about whether a canonical value existed to mirror. |
|
746
|
+ * Counting them would make the result unconditionally true, |
|
747
|
+ * which is the bug this return contract exists to fix. |
|
669
|
748
|
*/ |
|
670
|
749
|
private function sync_yoast($post_id, array $data) { |
|
750
|
+ if (!$this->third_party_writes_allowed()) { |
|
751
|
+ return false; |
|
752
|
+ } |
|
753
|
+ |
|
754
|
+ $wrote = false; |
|
755
|
+ |
|
671
|
756
|
// title |
|
672
|
757
|
if (!empty($data['title'])) { |
|
673
|
|
- update_post_meta($post_id, '_yoast_wpseo_title', (string) $data['title']); |
|
758
|
+ $wrote = $this->write_post_field($post_id, '_yoast_wpseo_title', (string) $data['title']); |
|
674
|
759
|
} |
|
675
|
760
|
|
|
676
|
761
|
// description -- strip newlines first |
|
677
|
762
|
if (!empty($data['desc'])) { |
|
678
|
763
|
$desc = str_replace(["\n", "\r", "\t"], ' ', $data['desc']); |
|
679
|
|
- update_post_meta($post_id, '_yoast_wpseo_metadesc', $desc); |
|
764
|
+ $wrote = $this->write_post_field($post_id, '_yoast_wpseo_metadesc', $desc) || $wrote; |
|
680
|
765
|
} |
|
681
|
766
|
|
|
682
|
767
|
// noindex: '0'=default, '1'=noindex, '2'=index |
|
683
|
768
|
if (array_key_exists('noindex', $data)) { |
|
684
|
769
|
$val = $data['noindex'] ? '1' : '2'; |
|
685
|
|
- update_post_meta($post_id, '_yoast_wpseo_meta-robots-noindex', $val); |
|
770
|
+ $this->write_post_field($post_id, '_yoast_wpseo_meta-robots-noindex', $val); |
|
686
|
771
|
} |
|
687
|
772
|
|
|
688
|
773
|
// nofollow: '0'=follow, '1'=nofollow |
|
689
|
774
|
if (array_key_exists('nofollow', $data)) { |
|
690
|
|
- 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'); |
|
691
|
776
|
} |
|
692
|
777
|
|
|
693
|
778
|
// advanced robots: comma-separated NO spaces |
|
694
|
779
|
$adv = []; |
|
@@ -700,38 +785,41 @@ |
|
700
|
785
|
} |
|
701
|
786
|
if (!empty($data['noimageindex'])) { |
|
702
|
787
|
$adv[] = 'noimageindex'; |
|
703
|
788
|
} |
|
704
|
|
- update_post_meta($post_id, '_yoast_wpseo_meta-robots-adv', implode(',', $adv)); |
|
789
|
+ // Written unconditionally so clearing the last directive clears the |
|
790
|
+ // field. Like the other robots writes it carries no canonical value, |
|
791
|
+ // so it does not make this a successful sync. |
|
792
|
+ $this->write_post_field($post_id, '_yoast_wpseo_meta-robots-adv', implode(',', $adv)); |
|
705
|
793
|
|
|
706
|
794
|
// OG |
|
707
|
795
|
if (!empty($data['og_title'])) { |
|
708
|
|
- update_post_meta($post_id, '_yoast_wpseo_opengraph-title', $data['og_title']); |
|
796
|
+ $wrote = $this->write_post_field($post_id, '_yoast_wpseo_opengraph-title', $data['og_title']) || $wrote; |
|
709
|
797
|
} |
|
710
|
798
|
if (!empty($data['og_desc'])) { |
|
711
|
|
- update_post_meta($post_id, '_yoast_wpseo_opengraph-description', $data['og_desc']); |
|
799
|
+ $wrote = $this->write_post_field($post_id, '_yoast_wpseo_opengraph-description', $data['og_desc']) || $wrote; |
|
712
|
800
|
} |
|
713
|
801
|
if (!empty($data['og_image'])) { |
|
714
|
|
- update_post_meta($post_id, '_yoast_wpseo_opengraph-image', esc_url_raw($data['og_image'])); |
|
802
|
+ $wrote = $this->write_post_field($post_id, '_yoast_wpseo_opengraph-image', esc_url_raw($data['og_image'])) || $wrote; |
|
715
|
803
|
} |
|
716
|
804
|
|
|
717
|
805
|
// Twitter |
|
718
|
806
|
if (!empty($data['twitter_title'])) { |
|
719
|
|
- update_post_meta($post_id, '_yoast_wpseo_twitter-title', $data['twitter_title']); |
|
807
|
+ $wrote = $this->write_post_field($post_id, '_yoast_wpseo_twitter-title', $data['twitter_title']) || $wrote; |
|
720
|
808
|
} |
|
721
|
809
|
if (!empty($data['twitter_desc'])) { |
|
722
|
|
- update_post_meta($post_id, '_yoast_wpseo_twitter-description', $data['twitter_desc']); |
|
810
|
+ $wrote = $this->write_post_field($post_id, '_yoast_wpseo_twitter-description', $data['twitter_desc']) || $wrote; |
|
723
|
811
|
} |
|
724
|
812
|
|
|
725
|
813
|
// Canonical, focus keyword, breadcrumb |
|
726
|
814
|
if (!empty($data['canonical'])) { |
|
727
|
|
- update_post_meta($post_id, '_yoast_wpseo_canonical', esc_url_raw($data['canonical'])); |
|
815
|
+ $wrote = $this->write_post_field($post_id, '_yoast_wpseo_canonical', esc_url_raw($data['canonical'])) || $wrote; |
|
728
|
816
|
} |
|
729
|
817
|
if (!empty($data['focus_keyword'])) { |
|
730
|
|
- update_post_meta($post_id, '_yoast_wpseo_focuskw', $data['focus_keyword']); |
|
818
|
+ $wrote = $this->write_post_field($post_id, '_yoast_wpseo_focuskw', $data['focus_keyword']) || $wrote; |
|
731
|
819
|
} |
|
732
|
820
|
if (!empty($data['breadcrumb_title'])) { |
|
733
|
|
- update_post_meta($post_id, '_yoast_wpseo_bctitle', $data['breadcrumb_title']); |
|
821
|
+ $wrote = $this->write_post_field($post_id, '_yoast_wpseo_bctitle', $data['breadcrumb_title']) || $wrote; |
|
734
|
822
|
} |
|
735
|
823
|
|
|
736
|
824
|
// Update wp_yoast_indexable cache row for immediate effect |
|
737
|
825
|
global $wpdb; |
|
@@ -769,9 +857,9 @@ |
|
769
|
857
|
$updates['twitter_title'] = mb_substr($data['twitter_title'], 0, 191); |
|
770
|
858
|
} |
|
771
|
859
|
if (!empty($data['twitter_card'])) { |
|
772
|
860
|
// twitter_card column only exists in newer Yoast versions; skip if absent |
|
773
|
|
- $col_check = $wpdb->get_var($wpdb->prepare( |
|
861
|
+ $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 |
|
774
|
862
|
"SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = %s AND TABLE_NAME = %s AND COLUMN_NAME = 'twitter_card'", |
|
775
|
863
|
DB_NAME, |
|
776
|
864
|
$indexable_table |
|
777
|
865
|
)); |
|
@@ -789,15 +877,38 @@ |
|
789
|
877
|
$updates['breadcrumb_title'] = mb_substr($data['breadcrumb_title'], 0, 191); |
|
790
|
878
|
} |
|
791
|
879
|
|
|
792
|
880
|
if (!empty($updates)) { |
|
793
|
|
- $row_exists = $wpdb->get_var($wpdb->prepare( |
|
794
|
|
- "SELECT id FROM {$indexable_table} WHERE object_id = %d AND object_type = 'post'", |
|
881
|
+ $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 |
|
882
|
+ "SELECT id FROM {$wpdb->prefix}yoast_indexable WHERE object_id = %d AND object_type = 'post'", |
|
795
|
883
|
$post_id |
|
796
|
884
|
)); |
|
797
|
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
|
+ |
|
798
|
909
|
if ($row_exists) { |
|
799
|
|
- $wpdb->update( |
|
910
|
+ $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 |
|
800
|
911
|
$indexable_table, |
|
801
|
912
|
$updates, |
|
802
|
913
|
['object_id' => $post_id, 'object_type' => 'post'] |
|
803
|
914
|
); |
|
@@ -817,12 +928,139 @@ |
|
817
|
928
|
'is_cornerstone' => 0, |
|
818
|
929
|
'created_at' => current_time('mysql'), |
|
819
|
930
|
'updated_at' => current_time('mysql'), |
|
820
|
931
|
], $updates); |
|
821
|
|
- $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) { // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- mirrors SEO meta into third-party plugin storage (Yoast indexables/AIOSEO tables) — no WordPress API for their schemas |
|
949
|
+ Metasync_Seo_Backup::discard_backups('post', $post_id, $backups_created); |
|
950
|
+ return $wrote; |
|
951
|
+ } |
|
822
|
952
|
} |
|
823
|
953
|
} |
|
824
|
954
|
|
|
955
|
+ return $wrote; |
|
956
|
+ } |
|
957
|
+ |
|
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
|
+ // Column list is esc_sql()'d above; the table is a fixed literal. |
|
1011
|
+ $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 |
|
1012
|
+ $wpdb->prepare( |
|
1013
|
+ // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- column list esc_sql()'d above, table is a fixed literal |
|
1014
|
+ "SELECT {$select} FROM {$wpdb->prefix}yoast_indexable WHERE object_id = %d AND object_type = 'post'", |
|
1015
|
+ $post_id |
|
1016
|
+ ), |
|
1017
|
+ ARRAY_A |
|
1018
|
+ ); |
|
1019
|
+ |
|
1020
|
+ // The row was there a moment ago, so a null answer now is a failed |
|
1021
|
+ // read, not an empty row. Recording it as "every column was NULL" |
|
1022
|
+ // would tell a later restore to delete values it should put back, |
|
1023
|
+ // which is the exact loss this layer exists to prevent. No write is |
|
1024
|
+ // happening, so the marker recorded above has to go too — a marker |
|
1025
|
+ // left describing a write that never ran is a stale story. |
|
1026
|
+ if ($current === null || !Metasync_Seo_Backup::db_read_succeeded()) { |
|
1027
|
+ Metasync_Seo_Backup::discard_backups('post', $post_id, $created); |
|
1028
|
+ return false; |
|
1029
|
+ } |
|
1030
|
+ } |
|
1031
|
+ |
|
1032
|
+ foreach ($columns as $column) { |
|
1033
|
+ // A missing row and a NULL column are the same thing to a restore: |
|
1034
|
+ // there was no value here, so put nothing back. |
|
1035
|
+ $current_value = ($current !== null && isset($current[$column])) ? $current[$column] : null; |
|
1036
|
+ |
|
1037
|
+ $field = 'yoast_indexable_' . $column; |
|
1038
|
+ |
|
1039
|
+ // One unsaved column is enough to refuse the whole write: a |
|
1040
|
+ // half-original, half-OTTO row is something no restore can unpick. |
|
1041
|
+ // The refusal also means the caller writes nothing, so withdraw the |
|
1042
|
+ // marker and the columns recorded so far — the same rule as the |
|
1043
|
+ // failed-insert path in sync_yoast(). Left behind, a row_existed='0' |
|
1044
|
+ // marker would let a restore delete a row the customer creates |
|
1045
|
+ // later, and a NULL column backup would blank a real value in it. |
|
1046
|
+ if (!Metasync_Seo_Backup::backup_before_overwrite( |
|
1047
|
+ 'post', |
|
1048
|
+ $post_id, |
|
1049
|
+ $field, |
|
1050
|
+ $updates[$column], |
|
1051
|
+ $current_value, |
|
1052
|
+ $column_created |
|
1053
|
+ )) { |
|
1054
|
+ Metasync_Seo_Backup::discard_backups('post', $post_id, $created); |
|
1055
|
+ return false; |
|
1056
|
+ } |
|
1057
|
+ |
|
1058
|
+ if ($column_created) { |
|
1059
|
+ $created[] = $field; |
|
1060
|
+ } |
|
1061
|
+ } |
|
1062
|
+ |
|
825
|
1063
|
return true; |
|
826
|
1064
|
} |
|
827
|
1065
|
|
|
828
|
1066
|
/** |
|
@@ -829,17 +1067,25 @@ |
|
829
|
1067
|
* Mirror canonical data into Rank Math post meta. |
|
830
|
1068
|
* |
|
831
|
1069
|
* @param int $post_id Post ID. |
|
832
|
1070
|
* @param array $data Canonical key/value pairs. |
|
833
|
|
- * @return bool True once dispatch completes. |
|
1071
|
+ * @return bool True when at least one value-bearing field was mirrored, |
|
1072
|
+ * false when the payload carried nothing to write. Robots |
|
1073
|
+ * directives are excluded for the reason given on sync_yoast(). |
|
834
|
1074
|
*/ |
|
835
|
1075
|
private function sync_rankmath($post_id, array $data) { |
|
1076
|
+ if (!$this->third_party_writes_allowed()) { |
|
1077
|
+ return false; |
|
1078
|
+ } |
|
1079
|
+ |
|
1080
|
+ $wrote = false; |
|
1081
|
+ |
|
836
|
1082
|
// title, desc |
|
837
|
1083
|
if (!empty($data['title'])) { |
|
838
|
|
- update_post_meta($post_id, 'rank_math_title', $data['title']); |
|
1084
|
+ $wrote = $this->write_post_field($post_id, 'rank_math_title', $data['title']); |
|
839
|
1085
|
} |
|
840
|
1086
|
if (!empty($data['desc'])) { |
|
841
|
|
- update_post_meta($post_id, 'rank_math_description', $data['desc']); |
|
1087
|
+ $wrote = $this->write_post_field($post_id, 'rank_math_description', $data['desc']) || $wrote; |
|
842
|
1088
|
} |
|
843
|
1089
|
|
|
844
|
1090
|
// robots: PHP indexed array |
|
845
|
1091
|
if (array_key_exists('noindex', $data) || array_key_exists('nofollow', $data)) { |
|
@@ -851,9 +1097,9 @@ |
|
851
|
1097
|
} |
|
852
|
1098
|
if (array_key_exists('nofollow', $data)) { |
|
853
|
1099
|
$robots[] = $data['nofollow'] ? 'nofollow' : 'follow'; |
|
854
|
1100
|
} |
|
855
|
|
- update_post_meta($post_id, 'rank_math_robots', array_values(array_unique($robots))); |
|
1101
|
+ $this->write_post_field($post_id, 'rank_math_robots', array_values(array_unique($robots))); |
|
856
|
1102
|
} |
|
857
|
1103
|
|
|
858
|
1104
|
// Advanced robots: max-* go into rank_math_advanced_robots |
|
859
|
1105
|
$adv_keys = ['max_snippet', 'max_image_preview', 'max_video_preview']; |
|
@@ -881,9 +1127,9 @@ |
|
881
|
1127
|
$val = (int) $data['max_video_preview']; |
|
882
|
1128
|
$adv['max-video-preview'] = (string) $val; |
|
883
|
1129
|
} |
|
884
|
1130
|
if (!empty($adv)) { |
|
885
|
|
- update_post_meta($post_id, 'rank_math_advanced_robots', $adv); |
|
1131
|
+ $this->write_post_field($post_id, 'rank_math_advanced_robots', $adv); |
|
886
|
1132
|
} |
|
887
|
1133
|
} |
|
888
|
1134
|
|
|
889
|
1135
|
// Sync noarchive/nosnippet/noimageindex into rank_math_robots |
|
@@ -898,51 +1144,164 @@ |
|
898
|
1144
|
if (!empty($data[$dir])) { |
|
899
|
1145
|
$robots[] = $dir; |
|
900
|
1146
|
} |
|
901
|
1147
|
} |
|
902
|
|
- update_post_meta($post_id, 'rank_math_robots', array_values(array_unique($robots))); |
|
1148
|
+ $this->write_post_field($post_id, 'rank_math_robots', array_values(array_unique($robots))); |
|
903
|
1149
|
} |
|
904
|
1150
|
|
|
905
|
1151
|
// OG |
|
906
|
1152
|
if (!empty($data['og_title'])) { |
|
907
|
|
- update_post_meta($post_id, 'rank_math_facebook_title', $data['og_title']); |
|
1153
|
+ $wrote = $this->write_post_field($post_id, 'rank_math_facebook_title', $data['og_title']) || $wrote; |
|
908
|
1154
|
} |
|
909
|
1155
|
if (!empty($data['og_desc'])) { |
|
910
|
|
- update_post_meta($post_id, 'rank_math_facebook_description', $data['og_desc']); |
|
1156
|
+ $wrote = $this->write_post_field($post_id, 'rank_math_facebook_description', $data['og_desc']) || $wrote; |
|
911
|
1157
|
} |
|
912
|
1158
|
if (!empty($data['og_image'])) { |
|
913
|
|
- update_post_meta($post_id, 'rank_math_facebook_image', esc_url_raw($data['og_image'])); |
|
1159
|
+ $wrote = $this->write_post_field($post_id, 'rank_math_facebook_image', esc_url_raw($data['og_image'])) || $wrote; |
|
914
|
1160
|
$img_id = attachment_url_to_postid($data['og_image']); |
|
915
|
1161
|
if ($img_id) { |
|
916
|
|
- update_post_meta($post_id, 'rank_math_facebook_image_id', $img_id); |
|
1162
|
+ $wrote = $this->write_post_field($post_id, 'rank_math_facebook_image_id', $img_id) || $wrote; |
|
917
|
1163
|
} |
|
918
|
1164
|
} |
|
919
|
1165
|
|
|
920
|
1166
|
// Twitter |
|
921
|
1167
|
if (!empty($data['twitter_title'])) { |
|
922
|
|
- update_post_meta($post_id, 'rank_math_twitter_title', $data['twitter_title']); |
|
1168
|
+ $wrote = $this->write_post_field($post_id, 'rank_math_twitter_title', $data['twitter_title']) || $wrote; |
|
923
|
1169
|
} |
|
924
|
1170
|
if (!empty($data['twitter_desc'])) { |
|
925
|
|
- update_post_meta($post_id, 'rank_math_twitter_description', $data['twitter_desc']); |
|
1171
|
+ $wrote = $this->write_post_field($post_id, 'rank_math_twitter_description', $data['twitter_desc']) || $wrote; |
|
926
|
1172
|
} |
|
927
|
1173
|
if (!empty($data['twitter_card'])) { |
|
928
|
1174
|
$valid_cards = ['summary', 'summary_large_image', 'app', 'player']; |
|
929
|
1175
|
if (in_array($data['twitter_card'], $valid_cards, true)) { |
|
930
|
|
- update_post_meta($post_id, 'rank_math_twitter_card_type', $data['twitter_card']); |
|
1176
|
+ $wrote = $this->write_post_field($post_id, 'rank_math_twitter_card_type', $data['twitter_card']) || $wrote; |
|
931
|
1177
|
} |
|
932
|
1178
|
} |
|
933
|
1179
|
|
|
934
|
1180
|
// Canonical, focus keyword, breadcrumb |
|
935
|
1181
|
if (!empty($data['canonical'])) { |
|
936
|
|
- update_post_meta($post_id, 'rank_math_canonical_url', esc_url_raw($data['canonical'])); |
|
1182
|
+ $wrote = $this->write_post_field($post_id, 'rank_math_canonical_url', esc_url_raw($data['canonical'])) || $wrote; |
|
937
|
1183
|
} |
|
938
|
1184
|
if (!empty($data['focus_keyword'])) { |
|
939
|
|
- update_post_meta($post_id, 'rank_math_focus_keyword', $data['focus_keyword']); |
|
1185
|
+ $wrote = $this->write_post_field($post_id, 'rank_math_focus_keyword', $data['focus_keyword']) || $wrote; |
|
940
|
1186
|
} |
|
941
|
1187
|
if (!empty($data['breadcrumb_title'])) { |
|
942
|
|
- update_post_meta($post_id, 'rank_math_breadcrumb_title', $data['breadcrumb_title']); |
|
1188
|
+ $wrote = $this->write_post_field($post_id, 'rank_math_breadcrumb_title', $data['breadcrumb_title']) || $wrote; |
|
943
|
1189
|
} |
|
944
|
1190
|
|
|
1191
|
+ return $wrote; |
|
1192
|
+ } |
|
1193
|
+ |
|
1194
|
+ /** |
|
1195
|
+ * Preserve the AIOSEO columns a sync is about to overwrite. |
|
1196
|
+ * |
|
1197
|
+ * AIOSEO keeps post SEO data in its own `aioseo_posts` table rather than in |
|
1198
|
+ * post meta, so there is no meta row to save and no field a restore could |
|
1199
|
+ * delete. Two things are recorded instead, both as post meta on the post |
|
1200
|
+ * itself: |
|
1201
|
+ * |
|
1202
|
+ * - one backup per column being written, so a restore can put the original |
|
1203
|
+ * values back and leave every other column of the user's row alone; |
|
1204
|
+ * - whether the row existed at all, so a restore can delete a row that only |
|
1205
|
+ * exists because we created it instead of leaving an empty shell behind. |
|
1206
|
+ * |
|
1207
|
+ * @param int $post_id Post ID. |
|
1208
|
+ * @param string $table Fully prefixed AIOSEO table name. |
|
1209
|
+ * @param array $row Columns and values about to be written. |
|
1210
|
+ * @param bool $row_existed Whether AIOSEO already had a row for this post. |
|
1211
|
+ * @param array $created Out-param, filled with the backup fields this |
|
1212
|
+ * call created, so a failed write can withdraw |
|
1213
|
+ * exactly its own rows and no one else's. |
|
1214
|
+ * @return bool True when every original was preserved and the caller may write. |
|
1215
|
+ */ |
|
1216
|
+ private function backup_aioseo_columns($post_id, $table, array $row, $row_existed, array &$created) { |
|
1217
|
+ $created = []; |
|
1218
|
+ |
|
1219
|
+ if (!class_exists('Metasync_Seo_Backup')) { |
|
1220
|
+ return false; |
|
1221
|
+ } |
|
1222
|
+ |
|
1223
|
+ global $wpdb; |
|
1224
|
+ |
|
1225
|
+ // Whether the row pre-existed is what a restore uses to choose between |
|
1226
|
+ // putting the original columns back and deleting a row that only exists |
|
1227
|
+ // because we made it. A marker that will not record is as disqualifying |
|
1228
|
+ // as a column that will not. |
|
1229
|
+ if (!Metasync_Seo_Backup::record_marker( |
|
1230
|
+ 'post', |
|
1231
|
+ $post_id, |
|
1232
|
+ 'aioseo_row_existed', |
|
1233
|
+ $row_existed ? '1' : '0', |
|
1234
|
+ $marker_created |
|
1235
|
+ )) { |
|
1236
|
+ return false; |
|
1237
|
+ } |
|
1238
|
+ |
|
1239
|
+ if ($marker_created) { |
|
1240
|
+ $created[] = 'aioseo_row_existed'; |
|
1241
|
+ } |
|
1242
|
+ |
|
1243
|
+ $columns = array_diff(array_keys($row), ['updated', 'created', 'post_id']); |
|
1244
|
+ if (empty($columns)) { |
|
1245
|
+ return true; |
|
1246
|
+ } |
|
1247
|
+ |
|
1248
|
+ $current = null; |
|
1249
|
+ if ($row_existed) { |
|
1250
|
+ $select = '`' . implode('`, `', array_map('esc_sql', $columns)) . '`'; |
|
1251
|
+ // Column list is esc_sql()'d above; the table is a fixed literal. |
|
1252
|
+ $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 |
|
1253
|
+ $wpdb->prepare( |
|
1254
|
+ // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- column list esc_sql()'d above, table is a fixed literal |
|
1255
|
+ "SELECT {$select} FROM {$wpdb->prefix}aioseo_posts WHERE post_id = %d", |
|
1256
|
+ $post_id |
|
1257
|
+ ), |
|
1258
|
+ ARRAY_A |
|
1259
|
+ ); |
|
1260
|
+ |
|
1261
|
+ // The row was there a moment ago, so a null answer now is a failed |
|
1262
|
+ // read, not an empty row. Recording it as "every column was NULL" |
|
1263
|
+ // would tell a later restore to delete values it should put back, |
|
1264
|
+ // which is the exact loss this layer exists to prevent. No write is |
|
1265
|
+ // happening, so the marker recorded above has to go too — a marker |
|
1266
|
+ // left describing a write that never ran is a stale story. |
|
1267
|
+ if ($current === null || !Metasync_Seo_Backup::db_read_succeeded()) { |
|
1268
|
+ Metasync_Seo_Backup::discard_backups('post', $post_id, $created); |
|
1269
|
+ return false; |
|
1270
|
+ } |
|
1271
|
+ } |
|
1272
|
+ |
|
1273
|
+ foreach ($columns as $column) { |
|
1274
|
+ // A missing row and a NULL column are the same thing to a restore: |
|
1275
|
+ // there was no value here, so put nothing back. |
|
1276
|
+ $current_value = ($current !== null && isset($current[$column])) ? $current[$column] : null; |
|
1277
|
+ |
|
1278
|
+ $field = 'aioseo_' . $column; |
|
1279
|
+ |
|
1280
|
+ // One unsaved column is enough to refuse the whole write: a half-original, |
|
1281
|
+ // half-OTTO row is something no restore can unpick. The refusal also |
|
1282
|
+ // means the caller writes nothing, so withdraw the marker and the |
|
1283
|
+ // columns recorded so far — the same rule as the failed-insert path |
|
1284
|
+ // in sync_aioseo(). Left behind, a row_existed='0' marker would let |
|
1285
|
+ // a restore delete a row the customer creates later, and a NULL |
|
1286
|
+ // column backup would blank a real value in it. |
|
1287
|
+ if (!Metasync_Seo_Backup::backup_before_overwrite( |
|
1288
|
+ 'post', |
|
1289
|
+ $post_id, |
|
1290
|
+ $field, |
|
1291
|
+ $row[$column], |
|
1292
|
+ $current_value, |
|
1293
|
+ $column_created |
|
1294
|
+ )) { |
|
1295
|
+ Metasync_Seo_Backup::discard_backups('post', $post_id, $created); |
|
1296
|
+ return false; |
|
1297
|
+ } |
|
1298
|
+ |
|
1299
|
+ if ($column_created) { |
|
1300
|
+ $created[] = $field; |
|
1301
|
+ } |
|
1302
|
+ } |
|
1303
|
+ |
|
945
|
1304
|
return true; |
|
946
|
1305
|
} |
|
947
|
1306
|
|
|
948
|
1307
|
/** |
|
@@ -949,54 +1308,73 @@ |
|
949
|
1308
|
* Mirror canonical data into the AIOSEO wp_aioseo_posts custom table. |
|
950
|
1309
|
* |
|
951
|
1310
|
* @param int $post_id Post ID. |
|
952
|
1311
|
* @param array $data Canonical key/value pairs. |
|
953
|
|
- * @return bool True when the row was written, false when the table is |
|
954
|
|
- * missing or the write failed. |
|
1312
|
+ * @return bool True when at least one value-bearing field was written, |
|
1313
|
+ * false when the table is missing, the write failed, or the |
|
1314
|
+ * payload carried only robots directives. Robots are excluded |
|
1315
|
+ * for the reason given on sync_yoast(), so the receipt means |
|
1316
|
+ * the same thing for all three plugins. |
|
955
|
1317
|
*/ |
|
956
|
1318
|
private function sync_aioseo($post_id, array $data) { |
|
957
|
1319
|
global $wpdb; |
|
958
|
1320
|
|
|
1321
|
+ if (!$this->third_party_writes_allowed()) { |
|
1322
|
+ return false; |
|
1323
|
+ } |
|
1324
|
+ |
|
959
|
1325
|
$table = $wpdb->prefix . 'aioseo_posts'; |
|
960
|
1326
|
|
|
961
|
1327
|
// Bail if the AIOSEO post table does not exist (plugin not initialised). |
|
962
|
|
- $table_exists = $wpdb->get_var($wpdb->prepare('SHOW TABLES LIKE %s', $table)); |
|
1328
|
+ $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 |
|
963
|
1329
|
if ($table_exists !== $table) { |
|
964
|
1330
|
return false; |
|
965
|
1331
|
} |
|
966
|
1332
|
|
|
967
|
1333
|
$row = []; |
|
1334
|
+ // Robots fill $row too, so a separate flag is needed: the row is still |
|
1335
|
+ // written for a robots-only payload, it just is not a content sync. |
|
1336
|
+ $wrote = false; |
|
968
|
1337
|
|
|
969
|
1338
|
if (!empty($data['title'])) { |
|
970
|
1339
|
$row['title'] = sanitize_text_field($data['title']); |
|
1340
|
+ $wrote = true; |
|
971
|
1341
|
} |
|
972
|
1342
|
if (!empty($data['desc'])) { |
|
973
|
1343
|
$row['description'] = sanitize_text_field($data['desc']); |
|
1344
|
+ $wrote = true; |
|
974
|
1345
|
} |
|
975
|
1346
|
if (!empty($data['og_title'])) { |
|
976
|
1347
|
$row['og_title'] = sanitize_text_field($data['og_title']); |
|
1348
|
+ $wrote = true; |
|
977
|
1349
|
} |
|
978
|
1350
|
if (!empty($data['og_desc'])) { |
|
979
|
1351
|
$row['og_description'] = sanitize_text_field($data['og_desc']); |
|
1352
|
+ $wrote = true; |
|
980
|
1353
|
} |
|
981
|
1354
|
if (!empty($data['og_image'])) { |
|
982
|
1355
|
$row['og_image_type'] = 'custom'; |
|
983
|
1356
|
$row['og_image_custom_url'] = esc_url_raw($data['og_image']); |
|
1357
|
+ $wrote = true; |
|
984
|
1358
|
} |
|
985
|
1359
|
if (!empty($data['twitter_title'])) { |
|
986
|
1360
|
$row['twitter_title'] = sanitize_text_field($data['twitter_title']); |
|
1361
|
+ $wrote = true; |
|
987
|
1362
|
} |
|
988
|
1363
|
if (!empty($data['twitter_desc'])) { |
|
989
|
1364
|
$row['twitter_description'] = sanitize_text_field($data['twitter_desc']); |
|
1365
|
+ $wrote = true; |
|
990
|
1366
|
} |
|
991
|
1367
|
if (!empty($data['twitter_card'])) { |
|
992
|
1368
|
$valid_cards = ['default', 'summary', 'summary_large_image', 'player', 'app']; |
|
993
|
1369
|
if (in_array($data['twitter_card'], $valid_cards, true)) { |
|
994
|
1370
|
$row['twitter_card'] = $data['twitter_card']; |
|
1371
|
+ $wrote = true; |
|
995
|
1372
|
} |
|
996
|
1373
|
} |
|
997
|
1374
|
if (!empty($data['canonical'])) { |
|
998
|
1375
|
$row['canonical_url'] = esc_url_raw($data['canonical']); |
|
1376
|
+ $wrote = true; |
|
999
|
1377
|
} |
|
1000
|
1378
|
|
|
1001
|
1379
|
// focus keyword as keyphrases JSON |
|
1002
|
1380
|
if (!empty($data['focus_keyword'])) { |
|
@@ -1007,8 +1385,9 @@ |
|
1007
|
1385
|
'analysis' => new \stdClass(), |
|
1008
|
1386
|
], |
|
1009
|
1387
|
'additional' => [], |
|
1010
|
1388
|
]); |
|
1389
|
+ $wrote = true; |
|
1011
|
1390
|
} |
|
1012
|
1391
|
|
|
1013
|
1392
|
// Robots |
|
1014
|
1393
|
$has_robots = false; |
|
@@ -1054,17 +1433,38 @@ |
|
1054
|
1433
|
} |
|
1055
|
1434
|
|
|
1056
|
1435
|
$row['updated'] = current_time('mysql'); |
|
1057
|
1436
|
|
|
1058
|
|
- $existing_id = $wpdb->get_var($wpdb->prepare( |
|
1059
|
|
- "SELECT id FROM {$table} WHERE post_id = %d", |
|
1437
|
+ $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 |
|
1438
|
+ "SELECT id FROM {$wpdb->prefix}aioseo_posts WHERE post_id = %d", |
|
1060
|
1439
|
$post_id |
|
1061
|
1440
|
)); |
|
1062
|
1441
|
|
|
1442
|
+ // An unreadable probe cannot be treated as "no row". It would commit a |
|
1443
|
+ // write-once row_existed='0' for a row AIOSEO really has -- which a |
|
1444
|
+ // restore reads as licence to delete it -- and send an INSERT at a row |
|
1445
|
+ // that already exists. Leave AIOSEO's row alone and let the next sync |
|
1446
|
+ // record the truth. |
|
1447
|
+ if (!Metasync_Seo_Backup::db_read_succeeded()) { |
|
1448
|
+ return false; |
|
1449
|
+ } |
|
1450
|
+ |
|
1451
|
+ // No original saved means no write. Overwriting anyway is the data loss |
|
1452
|
+ // this whole layer exists to prevent. |
|
1063
|
1453
|
if ($existing_id) { |
|
1064
|
|
- return $wpdb->update($table, $row, ['post_id' => $post_id]) !== false; |
|
1454
|
+ $backups_created = []; |
|
1455
|
+ if (!$this->backup_aioseo_columns($post_id, $table, $row, true, $backups_created)) { |
|
1456
|
+ return false; |
|
1457
|
+ } |
|
1458
|
+ |
|
1459
|
+ 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 |
|
1065
|
1460
|
} |
|
1066
|
1461
|
|
|
1462
|
+ $backups_created = []; |
|
1463
|
+ if (!$this->backup_aioseo_columns($post_id, $table, $row, false, $backups_created)) { |
|
1464
|
+ return false; |
|
1465
|
+ } |
|
1466
|
+ |
|
1067
|
1467
|
// New row -- must include all NOT NULL columns with no defaults |
|
1068
|
1468
|
$row['post_id'] = $post_id; |
|
1069
|
1469
|
$row['created'] = current_time('mysql'); |
|
1070
|
1470
|
$robot_defaults = [ |
|
@@ -1078,9 +1478,23 @@ |
|
1078
|
1478
|
'robots_notranslate' => 0, |
|
1079
|
1479
|
]; |
|
1080
|
1480
|
$row = array_merge($robot_defaults, $row); |
|
1081
|
1481
|
|
|
1082
|
|
- return $wpdb->insert($table, $row) !== false; |
|
1482
|
+ $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 |
|
1483
|
+ |
|
1484
|
+ // The row_existed='0' marker was recorded before the insert, because a |
|
1485
|
+ // marker that will not save has to be able to veto the write. If the |
|
1486
|
+ // insert then failed there is no row of ours, and leaving the marker |
|
1487
|
+ // behind would let a restore delete a row the customer creates later. |
|
1488
|
+ // The per-column backups beside it go too, or a column captured as NULL |
|
1489
|
+ // for a row that never existed would blank a real value the customer |
|
1490
|
+ // later puts in one. |
|
1491
|
+ if ($inserted === false) { |
|
1492
|
+ Metasync_Seo_Backup::discard_backups('post', $post_id, $backups_created); |
|
1493
|
+ return false; |
|
1494
|
+ } |
|
1495
|
+ |
|
1496
|
+ return $wrote; |
|
1083
|
1497
|
} |
|
1084
|
1498
|
|
|
1085
|
1499
|
// ------------------------------------------------------------------ |
|
1086
|
1500
|
// Two-way sync: sidebar JSON ↔ legacy meta boxes |