| 1 |
<?php |
| 2 |
/** |
| 3 |
* Post-Level SEO Plugin Sync |
| 4 |
* |
| 5 |
* Mirrors MetaSync post meta (`_metasync_*`) into the active third-party |
| 6 |
* SEO plugins' post storage (Yoast, Rank Math, AIOSEO) so that posts and |
| 7 |
* pages render MetaSync-managed values regardless of which plugin is |
| 8 |
* actually rendering the frontend. |
| 9 |
* |
| 10 |
* @package MetaSync |
| 11 |
* @subpackage MetaSync/includes |
| 12 |
* @since 2.8.25 |
| 13 |
*/ |
| 14 |
|
| 15 |
if (!defined('ABSPATH')) { |
| 16 |
exit; |
| 17 |
} |
| 18 |
|
| 19 |
class Metasync_Plugin_Sync { |
| 20 |
|
| 21 |
/** |
| 22 |
* Singleton instance. |
| 23 |
* |
| 24 |
* @var self|null |
| 25 |
*/ |
| 26 |
private static $instance = null; |
| 27 |
|
| 28 |
/** |
| 29 |
* Guard: tracks post IDs currently being synced from JSON → legacy. |
| 30 |
* Prevents sync_legacy_to_json from overwriting the correct JSON value. |
| 31 |
* |
| 32 |
* @var array |
| 33 |
*/ |
| 34 |
private $syncing_json_to_legacy = []; |
| 35 |
|
| 36 |
/** |
| 37 |
* MetaSync meta keys that trigger a sync when written. |
| 38 |
* |
| 39 |
* @var array |
| 40 |
*/ |
| 41 |
const WATCHED_KEYS = [ |
| 42 |
// Sidebar / persisted keys |
| 43 |
'_metasync_seo_title', |
| 44 |
'_metasync_seo_desc', |
| 45 |
'_metasync_metatitle', |
| 46 |
'_metasync_metadesc', |
| 47 |
'_metasync_robots_index', |
| 48 |
'_metasync_robots_advanced', |
| 49 |
'_metasync_og_title', |
| 50 |
'_metasync_og_description', |
| 51 |
'_metasync_og_image', |
| 52 |
'_metasync_twitter_title', |
| 53 |
'_metasync_twitter_description', |
| 54 |
'_metasync_twitter_card', |
| 55 |
'_metasync_canonical_url', |
| 56 |
'_metasync_focus_keyword', |
| 57 |
'_metasync_breadcrumb_title', |
| 58 |
// OTTO volatile keys |
| 59 |
'_metasync_otto_title', |
| 60 |
'_metasync_otto_description', |
| 61 |
'_metasync_otto_og_title', |
| 62 |
'_metasync_otto_og_description', |
| 63 |
'_metasync_otto_twitter_title', |
| 64 |
'_metasync_otto_twitter_description', |
| 65 |
'_metasync_otto_keywords', |
| 66 |
]; |
| 67 |
|
| 68 |
/** |
| 69 |
* Get singleton instance. |
| 70 |
* |
| 71 |
* @return self |
| 72 |
*/ |
| 73 |
public static function get_instance() { |
| 74 |
if (self::$instance === null) { |
| 75 |
self::$instance = new self(); |
| 76 |
} |
| 77 |
return self::$instance; |
| 78 |
} |
| 79 |
|
| 80 |
/** |
| 81 |
* Sync MetaSync post meta to every active SEO plugin. |
| 82 |
* |
| 83 |
* When $fields is non-empty only those canonical keys are synced. |
| 84 |
* When empty a full sync of all canonical keys is performed. |
| 85 |
* |
| 86 |
* @param int $post_id Post ID. |
| 87 |
* @param array $fields Optional subset of canonical key/value pairs to sync. |
| 88 |
* @return array Results keyed by plugin: ['yoast'=>bool,'rankmath'=>bool,'aioseo'=>bool]. |
| 89 |
*/ |
| 90 |
public function sync_post($post_id, array $fields = []) { |
| 91 |
static $syncing = []; |
| 92 |
|
| 93 |
if (!empty($syncing[$post_id])) { |
| 94 |
return []; |
| 95 |
} |
| 96 |
$syncing[$post_id] = true; |
| 97 |
|
| 98 |
try { |
| 99 |
$results = []; |
| 100 |
|
| 101 |
if ($post_id <= 0) { |
| 102 |
return $results; |
| 103 |
} |
| 104 |
|
| 105 |
$data = $this->collect_post_data($post_id, $fields); |
| 106 |
|
| 107 |
if (empty($data)) { |
| 108 |
return $results; |
| 109 |
} |
| 110 |
|
| 111 |
if ($this->is_yoast_active()) { |
| 112 |
$results['yoast'] = $this->sync_yoast((int) $post_id, $data); |
| 113 |
} |
| 114 |
|
| 115 |
if ($this->is_rankmath_active()) { |
| 116 |
$results['rankmath'] = $this->sync_rankmath((int) $post_id, $data); |
| 117 |
} |
| 118 |
|
| 119 |
if ($this->is_aioseo_active()) { |
| 120 |
$results['aioseo'] = $this->sync_aioseo((int) $post_id, $data); |
| 121 |
} |
| 122 |
|
| 123 |
// Write sync timestamp |
| 124 |
$ts = get_post_meta($post_id, '_metasync_plugin_sync_ts', true); |
| 125 |
$sync_data = !empty($ts) ? json_decode($ts, true) : []; |
| 126 |
if (!is_array($sync_data)) { |
| 127 |
$sync_data = []; |
| 128 |
} |
| 129 |
if ($results['yoast'] ?? false) { |
| 130 |
$sync_data['yoast'] = gmdate('c'); |
| 131 |
} |
| 132 |
if ($results['rankmath'] ?? false) { |
| 133 |
$sync_data['rankmath'] = gmdate('c'); |
| 134 |
} |
| 135 |
if ($results['aioseo'] ?? false) { |
| 136 |
$sync_data['aioseo'] = gmdate('c'); |
| 137 |
} |
| 138 |
if (!empty($sync_data)) { |
| 139 |
update_post_meta($post_id, '_metasync_plugin_sync_ts', wp_json_encode($sync_data)); |
| 140 |
} |
| 141 |
|
| 142 |
return $results; |
| 143 |
} finally { |
| 144 |
unset($syncing[$post_id]); |
| 145 |
} |
| 146 |
} |
| 147 |
|
| 148 |
/** |
| 149 |
* Hook handler for updated_post_meta / added_post_meta. |
| 150 |
* |
| 151 |
* Fires sync_post when a watched MetaSync meta key is written. |
| 152 |
* |
| 153 |
* @param int $meta_id Meta row ID (unused). |
| 154 |
* @param int $post_id Post ID. |
| 155 |
* @param string $meta_key Meta key being written. |
| 156 |
* @param mixed $meta_value Meta value being written. |
| 157 |
*/ |
| 158 |
public function on_meta_updated($meta_id, $post_id, $meta_key, $meta_value) { |
| 159 |
$watched = [ |
| 160 |
// Sidebar / persisted keys |
| 161 |
'_metasync_seo_title' => 'title', |
| 162 |
'_metasync_seo_desc' => 'desc', |
| 163 |
'_metasync_metatitle' => 'title', |
| 164 |
'_metasync_metadesc' => 'desc', |
| 165 |
'_metasync_robots_index' => 'noindex', |
| 166 |
'_metasync_og_title' => 'og_title', |
| 167 |
'_metasync_og_description' => 'og_desc', |
| 168 |
'_metasync_og_image' => 'og_image', |
| 169 |
'_metasync_twitter_title' => 'twitter_title', |
| 170 |
'_metasync_twitter_description' => 'twitter_desc', |
| 171 |
'_metasync_twitter_card' => 'twitter_card', |
| 172 |
'_metasync_canonical_url' => 'canonical', |
| 173 |
'_metasync_focus_keyword' => 'focus_keyword', |
| 174 |
'_metasync_breadcrumb_title' => 'breadcrumb_title', |
| 175 |
'_metasync_robots_advanced' => '_robots_advanced_json', |
| 176 |
// OTTO volatile keys (SSR writes these even without persistence) |
| 177 |
'_metasync_otto_title' => 'title', |
| 178 |
'_metasync_otto_description' => 'desc', |
| 179 |
'_metasync_otto_og_title' => 'og_title', |
| 180 |
'_metasync_otto_og_description' => 'og_desc', |
| 181 |
'_metasync_otto_twitter_title' => 'twitter_title', |
| 182 |
'_metasync_otto_twitter_description' => 'twitter_desc', |
| 183 |
'_metasync_otto_keywords' => 'focus_keyword', |
| 184 |
// Legacy meta box keys → rebuild JSON |
| 185 |
'metasync_common_robots' => '_legacy_robots_to_json', |
| 186 |
'metasync_advance_robots' => '_legacy_robots_to_json', |
| 187 |
]; |
| 188 |
|
| 189 |
if (!isset($watched[$meta_key])) { |
| 190 |
return; |
| 191 |
} |
| 192 |
|
| 193 |
$canonical_key = $watched[$meta_key]; |
| 194 |
|
| 195 |
// JSON key -- do a full sync + mirror to legacy meta boxes |
| 196 |
if ($canonical_key === '_robots_advanced_json') { |
| 197 |
$this->sync_post((int) $post_id); |
| 198 |
$this->sync_to_legacy_meta((int) $post_id, $meta_value); |
| 199 |
// Re-sync at shutdown only when Yoast is active — Yoast's indexable |
| 200 |
// watcher can overwrite our values during the same request. |
| 201 |
if ($this->is_yoast_active()) { |
| 202 |
$sync_instance = $this; |
| 203 |
$sync_post_id = (int) $post_id; |
| 204 |
add_action('shutdown', function() use ($sync_instance, $sync_post_id) { |
| 205 |
$sync_instance->sync_post($sync_post_id); |
| 206 |
}, 0); |
| 207 |
} |
| 208 |
return; |
| 209 |
} |
| 210 |
|
| 211 |
// Legacy meta box → rebuild _metasync_robots_advanced JSON |
| 212 |
if ($canonical_key === '_legacy_robots_to_json') { |
| 213 |
$this->sync_legacy_to_json((int) $post_id); |
| 214 |
return; |
| 215 |
} |
| 216 |
|
| 217 |
// For noindex: convert 'noindex' string to bool |
| 218 |
if ($canonical_key === 'noindex') { |
| 219 |
$value = ($meta_value === 'noindex'); |
| 220 |
} else { |
| 221 |
$value = $meta_value; |
| 222 |
} |
| 223 |
|
| 224 |
$this->sync_post( |
| 225 |
(int) $post_id, |
| 226 |
[$canonical_key => $value] |
| 227 |
); |
| 228 |
} |
| 229 |
|
| 230 |
// ------------------------------------------------------------------ |
| 231 |
// Data collection |
| 232 |
// ------------------------------------------------------------------ |
| 233 |
|
| 234 |
/** |
| 235 |
* Collect canonical SEO data from all MetaSync post meta. |
| 236 |
* |
| 237 |
* Reads all meta in one get_post_custom() call for performance. |
| 238 |
* When $fields is non-empty, the result is filtered to only those keys. |
| 239 |
* |
| 240 |
* @param int $post_id Post ID. |
| 241 |
* @param array $fields Optional pre-resolved canonical key/value pairs. |
| 242 |
* @return array Canonical data array. |
| 243 |
*/ |
| 244 |
private function collect_post_data($post_id, array $fields = []) { |
| 245 |
// If caller already resolved specific fields, return them directly. |
| 246 |
// Canonical still gets validated — this branch serves the |
| 247 |
// updated_post_meta fast-path, which would otherwise mirror a raw |
| 248 |
// (possibly corrupted) value into third-party storage. |
| 249 |
if (!empty($fields)) { |
| 250 |
if (array_key_exists('canonical', $fields)) { |
| 251 |
$fields['canonical'] = Metasync_Canonical_Sanitizer::sanitize($fields['canonical']); |
| 252 |
if ($fields['canonical'] === '') { |
| 253 |
unset($fields['canonical']); |
| 254 |
} |
| 255 |
} |
| 256 |
return $fields; |
| 257 |
} |
| 258 |
|
| 259 |
$all_meta = get_post_custom($post_id); |
| 260 |
|
| 261 |
$get = function ($key) use ($all_meta) { |
| 262 |
if (!isset($all_meta[$key])) { |
| 263 |
return ''; |
| 264 |
} |
| 265 |
return is_array($all_meta[$key]) ? $all_meta[$key][0] : $all_meta[$key]; |
| 266 |
}; |
| 267 |
|
| 268 |
$data = []; |
| 269 |
|
| 270 |
// Helper: first non-empty value from a list of meta keys |
| 271 |
$first = function (...$keys) use ($get) { |
| 272 |
foreach ($keys as $key) { |
| 273 |
$val = $get($key); |
| 274 |
if (!empty($val)) { |
| 275 |
return $val; |
| 276 |
} |
| 277 |
} |
| 278 |
return ''; |
| 279 |
}; |
| 280 |
|
| 281 |
// title: sidebar > persisted OTTO > volatile OTTO |
| 282 |
$data['title'] = $first('_metasync_seo_title', '_metasync_metatitle', '_metasync_otto_title'); |
| 283 |
|
| 284 |
// desc: sidebar > persisted OTTO > volatile OTTO |
| 285 |
$data['desc'] = $first('_metasync_seo_desc', '_metasync_metadesc', '_metasync_otto_description'); |
| 286 |
|
| 287 |
// Robots directives: check _metasync_robots_advanced JSON first, |
| 288 |
// then fall back to metasync_common_robots array + metasync_advance_robots array |
| 289 |
$robots_json_raw = $get('_metasync_robots_advanced'); |
| 290 |
$robots_json = !empty($robots_json_raw) ? json_decode($robots_json_raw, true) : null; |
| 291 |
|
| 292 |
if (is_array($robots_json)) { |
| 293 |
$data['noindex'] = !empty($robots_json['noindex']); |
| 294 |
$data['nofollow'] = !empty($robots_json['nofollow']); |
| 295 |
$data['noarchive'] = !empty($robots_json['noarchive']); |
| 296 |
$data['nosnippet'] = !empty($robots_json['nosnippet']); |
| 297 |
$data['noimageindex'] = !empty($robots_json['noimageindex']); |
| 298 |
$data['max_snippet'] = isset($robots_json['max_snippet']) ? (int) $robots_json['max_snippet'] : (isset($robots_json['max-snippet']) ? (int) $robots_json['max-snippet'] : null); |
| 299 |
$data['max_image_preview'] = $robots_json['max_image_preview'] ?? $robots_json['max-image-preview'] ?? null; |
| 300 |
$data['max_video_preview'] = isset($robots_json['max_video_preview']) ? (int) $robots_json['max_video_preview'] : (isset($robots_json['max-video-preview']) ? (int) $robots_json['max-video-preview'] : null); |
| 301 |
} else { |
| 302 |
// noindex from dedicated key |
| 303 |
$robots_index = $get('_metasync_robots_index'); |
| 304 |
$data['noindex'] = ($robots_index === 'noindex'); |
| 305 |
|
| 306 |
// Common robots array (serialized) |
| 307 |
$common_raw = $get('metasync_common_robots'); |
| 308 |
$common_robots = !empty($common_raw) ? maybe_unserialize($common_raw) : []; |
| 309 |
if (!is_array($common_robots)) { |
| 310 |
$common_robots = []; |
| 311 |
} |
| 312 |
|
| 313 |
$data['nofollow'] = !empty($common_robots['nofollow']); |
| 314 |
$data['noarchive'] = !empty($common_robots['noarchive']); |
| 315 |
$data['nosnippet'] = !empty($common_robots['nosnippet']); |
| 316 |
$data['noimageindex'] = !empty($common_robots['noimageindex']); |
| 317 |
|
| 318 |
// Advance robots array (serialized) |
| 319 |
$adv_raw = $get('metasync_advance_robots'); |
| 320 |
$adv_robots = !empty($adv_raw) ? maybe_unserialize($adv_raw) : []; |
| 321 |
if (!is_array($adv_robots)) { |
| 322 |
$adv_robots = []; |
| 323 |
} |
| 324 |
|
| 325 |
$data['max_snippet'] = isset($adv_robots['max-snippet']) ? (int) $adv_robots['max-snippet'] : null; |
| 326 |
$data['max_image_preview'] = isset($adv_robots['max-image-preview']) ? $adv_robots['max-image-preview'] : null; |
| 327 |
$data['max_video_preview'] = isset($adv_robots['max-video-preview']) ? (int) $adv_robots['max-video-preview'] : null; |
| 328 |
} |
| 329 |
|
| 330 |
// Social / OG: persisted > volatile OTTO |
| 331 |
$data['og_title'] = $first('_metasync_og_title', '_metasync_otto_og_title'); |
| 332 |
$data['og_desc'] = $first('_metasync_og_description', '_metasync_otto_og_description'); |
| 333 |
$data['og_image'] = $get('_metasync_og_image'); |
| 334 |
|
| 335 |
// Twitter: persisted > volatile OTTO |
| 336 |
$data['twitter_title'] = $first('_metasync_twitter_title', '_metasync_otto_twitter_title'); |
| 337 |
$data['twitter_desc'] = $first('_metasync_twitter_description', '_metasync_otto_twitter_description'); |
| 338 |
$data['twitter_card'] = $get('_metasync_twitter_card'); |
| 339 |
|
| 340 |
// Canonical, focus keyword, breadcrumb |
| 341 |
// Canonical is validated at the source so a corrupted value ("Array") |
| 342 |
// never propagates into Yoast/RankMath/AIOSEO storage. |
| 343 |
$data['canonical'] = Metasync_Canonical_Sanitizer::sanitize($get('_metasync_canonical_url')); |
| 344 |
$data['focus_keyword'] = $first('_metasync_focus_keyword', '_metasync_otto_keywords'); |
| 345 |
$data['breadcrumb_title'] = $get('_metasync_breadcrumb_title'); |
| 346 |
|
| 347 |
return $data; |
| 348 |
} |
| 349 |
|
| 350 |
// ------------------------------------------------------------------ |
| 351 |
// Plugin detectors |
| 352 |
// ------------------------------------------------------------------ |
| 353 |
|
| 354 |
/** |
| 355 |
* Check whether Yoast SEO (free or premium) is active. |
| 356 |
* |
| 357 |
* @return bool |
| 358 |
*/ |
| 359 |
private function is_yoast_active() { |
| 360 |
$this->ensure_plugin_api(); |
| 361 |
return is_plugin_active('wordpress-seo/wp-seo.php') |
| 362 |
|| is_plugin_active('wordpress-seo-premium/wp-seo-premium.php'); |
| 363 |
} |
| 364 |
|
| 365 |
/** |
| 366 |
* Check whether Rank Math SEO is active. |
| 367 |
* |
| 368 |
* @return bool |
| 369 |
*/ |
| 370 |
private function is_rankmath_active() { |
| 371 |
$this->ensure_plugin_api(); |
| 372 |
return is_plugin_active('seo-by-rank-math/rank-math.php') |
| 373 |
|| is_plugin_active('seo-by-rankmath/rank-math.php'); |
| 374 |
} |
| 375 |
|
| 376 |
/** |
| 377 |
* Check whether AIOSEO (free or pro) is active. |
| 378 |
* |
| 379 |
* @return bool |
| 380 |
*/ |
| 381 |
private function is_aioseo_active() { |
| 382 |
$this->ensure_plugin_api(); |
| 383 |
return is_plugin_active('all-in-one-seo-pack/all_in_one_seo_pack.php') |
| 384 |
|| is_plugin_active('all-in-one-seo-pack-pro/all_in_one_seo_pack.php'); |
| 385 |
} |
| 386 |
|
| 387 |
/** |
| 388 |
* Ensure is_plugin_active() is loaded on the frontend. |
| 389 |
*/ |
| 390 |
private function ensure_plugin_api() { |
| 391 |
if (!function_exists('is_plugin_active')) { |
| 392 |
require_once ABSPATH . 'wp-admin/includes/plugin.php'; |
| 393 |
} |
| 394 |
} |
| 395 |
|
| 396 |
// ------------------------------------------------------------------ |
| 397 |
// Per-plugin sync |
| 398 |
// ------------------------------------------------------------------ |
| 399 |
|
| 400 |
/** |
| 401 |
* Mirror canonical data into Yoast post meta and indexable cache. |
| 402 |
* |
| 403 |
* @param int $post_id Post ID. |
| 404 |
* @param array $data Canonical key/value pairs. |
| 405 |
* @return bool True once dispatch completes. |
| 406 |
*/ |
| 407 |
private function sync_yoast($post_id, array $data) { |
| 408 |
// title |
| 409 |
if (!empty($data['title'])) { |
| 410 |
update_post_meta($post_id, '_yoast_wpseo_title', (string) $data['title']); |
| 411 |
} |
| 412 |
|
| 413 |
// description -- strip newlines first |
| 414 |
if (!empty($data['desc'])) { |
| 415 |
$desc = str_replace(["\n", "\r", "\t"], ' ', $data['desc']); |
| 416 |
update_post_meta($post_id, '_yoast_wpseo_metadesc', $desc); |
| 417 |
} |
| 418 |
|
| 419 |
// noindex: '0'=default, '1'=noindex, '2'=index |
| 420 |
if (array_key_exists('noindex', $data)) { |
| 421 |
$val = $data['noindex'] ? '1' : '2'; |
| 422 |
update_post_meta($post_id, '_yoast_wpseo_meta-robots-noindex', $val); |
| 423 |
} |
| 424 |
|
| 425 |
// nofollow: '0'=follow, '1'=nofollow |
| 426 |
if (array_key_exists('nofollow', $data)) { |
| 427 |
update_post_meta($post_id, '_yoast_wpseo_meta-robots-nofollow', $data['nofollow'] ? '1' : '0'); |
| 428 |
} |
| 429 |
|
| 430 |
// advanced robots: comma-separated NO spaces |
| 431 |
$adv = []; |
| 432 |
if (!empty($data['noarchive'])) { |
| 433 |
$adv[] = 'noarchive'; |
| 434 |
} |
| 435 |
if (!empty($data['nosnippet'])) { |
| 436 |
$adv[] = 'nosnippet'; |
| 437 |
} |
| 438 |
if (!empty($data['noimageindex'])) { |
| 439 |
$adv[] = 'noimageindex'; |
| 440 |
} |
| 441 |
update_post_meta($post_id, '_yoast_wpseo_meta-robots-adv', implode(',', $adv)); |
| 442 |
|
| 443 |
// OG |
| 444 |
if (!empty($data['og_title'])) { |
| 445 |
update_post_meta($post_id, '_yoast_wpseo_opengraph-title', $data['og_title']); |
| 446 |
} |
| 447 |
if (!empty($data['og_desc'])) { |
| 448 |
update_post_meta($post_id, '_yoast_wpseo_opengraph-description', $data['og_desc']); |
| 449 |
} |
| 450 |
if (!empty($data['og_image'])) { |
| 451 |
update_post_meta($post_id, '_yoast_wpseo_opengraph-image', esc_url_raw($data['og_image'])); |
| 452 |
} |
| 453 |
|
| 454 |
// Twitter |
| 455 |
if (!empty($data['twitter_title'])) { |
| 456 |
update_post_meta($post_id, '_yoast_wpseo_twitter-title', $data['twitter_title']); |
| 457 |
} |
| 458 |
if (!empty($data['twitter_desc'])) { |
| 459 |
update_post_meta($post_id, '_yoast_wpseo_twitter-description', $data['twitter_desc']); |
| 460 |
} |
| 461 |
|
| 462 |
// Canonical, focus keyword, breadcrumb |
| 463 |
if (!empty($data['canonical'])) { |
| 464 |
update_post_meta($post_id, '_yoast_wpseo_canonical', esc_url_raw($data['canonical'])); |
| 465 |
} |
| 466 |
if (!empty($data['focus_keyword'])) { |
| 467 |
update_post_meta($post_id, '_yoast_wpseo_focuskw', $data['focus_keyword']); |
| 468 |
} |
| 469 |
if (!empty($data['breadcrumb_title'])) { |
| 470 |
update_post_meta($post_id, '_yoast_wpseo_bctitle', $data['breadcrumb_title']); |
| 471 |
} |
| 472 |
|
| 473 |
// Update wp_yoast_indexable cache row for immediate effect |
| 474 |
global $wpdb; |
| 475 |
$indexable_table = $wpdb->prefix . 'yoast_indexable'; |
| 476 |
|
| 477 |
$updates = []; |
| 478 |
if (!empty($data['title'])) { |
| 479 |
$updates['title'] = mb_substr($data['title'], 0, 191); |
| 480 |
} |
| 481 |
if (!empty($data['desc'])) { |
| 482 |
$updates['description'] = str_replace(["\n", "\r", "\t"], ' ', $data['desc']); |
| 483 |
} |
| 484 |
if (array_key_exists('noindex', $data)) { |
| 485 |
$updates['is_robots_noindex'] = $data['noindex'] ? 1 : 0; |
| 486 |
} |
| 487 |
if (array_key_exists('nofollow', $data)) { |
| 488 |
$updates['is_robots_nofollow'] = $data['nofollow'] ? 1 : 0; |
| 489 |
} |
| 490 |
if (array_key_exists('noarchive', $data)) { |
| 491 |
$updates['is_robots_noarchive'] = !empty($data['noarchive']) ? 1 : 0; |
| 492 |
} |
| 493 |
if (array_key_exists('nosnippet', $data)) { |
| 494 |
$updates['is_robots_nosnippet'] = !empty($data['nosnippet']) ? 1 : 0; |
| 495 |
} |
| 496 |
if (array_key_exists('noimageindex', $data)) { |
| 497 |
$updates['is_robots_noimageindex'] = !empty($data['noimageindex']) ? 1 : 0; |
| 498 |
} |
| 499 |
if (!empty($data['og_title'])) { |
| 500 |
$updates['open_graph_title'] = mb_substr($data['og_title'], 0, 191); |
| 501 |
} |
| 502 |
if (!empty($data['og_image'])) { |
| 503 |
$updates['open_graph_image'] = $data['og_image']; |
| 504 |
} |
| 505 |
if (!empty($data['twitter_title'])) { |
| 506 |
$updates['twitter_title'] = mb_substr($data['twitter_title'], 0, 191); |
| 507 |
} |
| 508 |
if (!empty($data['twitter_card'])) { |
| 509 |
// twitter_card column only exists in newer Yoast versions; skip if absent |
| 510 |
$col_check = $wpdb->get_var($wpdb->prepare( |
| 511 |
"SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = %s AND TABLE_NAME = %s AND COLUMN_NAME = 'twitter_card'", |
| 512 |
DB_NAME, |
| 513 |
$indexable_table |
| 514 |
)); |
| 515 |
if ($col_check) { |
| 516 |
$updates['twitter_card'] = $data['twitter_card']; |
| 517 |
} |
| 518 |
} |
| 519 |
if (!empty($data['canonical'])) { |
| 520 |
$updates['canonical'] = $data['canonical']; |
| 521 |
} |
| 522 |
if (!empty($data['focus_keyword'])) { |
| 523 |
$updates['primary_focus_keyword'] = mb_substr($data['focus_keyword'], 0, 191); |
| 524 |
} |
| 525 |
if (!empty($data['breadcrumb_title'])) { |
| 526 |
$updates['breadcrumb_title'] = mb_substr($data['breadcrumb_title'], 0, 191); |
| 527 |
} |
| 528 |
|
| 529 |
if (!empty($updates)) { |
| 530 |
$row_exists = $wpdb->get_var($wpdb->prepare( |
| 531 |
"SELECT id FROM {$indexable_table} WHERE object_id = %d AND object_type = 'post'", |
| 532 |
$post_id |
| 533 |
)); |
| 534 |
|
| 535 |
if ($row_exists) { |
| 536 |
$wpdb->update( |
| 537 |
$indexable_table, |
| 538 |
$updates, |
| 539 |
['object_id' => $post_id, 'object_type' => 'post'] |
| 540 |
); |
| 541 |
} else { |
| 542 |
$post = get_post($post_id); |
| 543 |
$insert = array_merge([ |
| 544 |
'object_id' => $post_id, |
| 545 |
'object_type' => 'post', |
| 546 |
'object_sub_type' => $post ? $post->post_type : 'post', |
| 547 |
'post_status' => $post ? $post->post_status : 'publish', |
| 548 |
'author_id' => $post ? (int) $post->post_author : 0, |
| 549 |
'is_robots_noindex' => 0, |
| 550 |
'is_robots_nofollow' => 0, |
| 551 |
'is_robots_noarchive' => 0, |
| 552 |
'is_robots_nosnippet' => 0, |
| 553 |
'is_robots_noimageindex' => 0, |
| 554 |
'is_cornerstone' => 0, |
| 555 |
'created_at' => current_time('mysql'), |
| 556 |
'updated_at' => current_time('mysql'), |
| 557 |
], $updates); |
| 558 |
$wpdb->insert($indexable_table, $insert); |
| 559 |
} |
| 560 |
} |
| 561 |
|
| 562 |
return true; |
| 563 |
} |
| 564 |
|
| 565 |
/** |
| 566 |
* Mirror canonical data into Rank Math post meta. |
| 567 |
* |
| 568 |
* @param int $post_id Post ID. |
| 569 |
* @param array $data Canonical key/value pairs. |
| 570 |
* @return bool True once dispatch completes. |
| 571 |
*/ |
| 572 |
private function sync_rankmath($post_id, array $data) { |
| 573 |
// title, desc |
| 574 |
if (!empty($data['title'])) { |
| 575 |
update_post_meta($post_id, 'rank_math_title', $data['title']); |
| 576 |
} |
| 577 |
if (!empty($data['desc'])) { |
| 578 |
update_post_meta($post_id, 'rank_math_description', $data['desc']); |
| 579 |
} |
| 580 |
|
| 581 |
// robots: PHP indexed array |
| 582 |
if (array_key_exists('noindex', $data) || array_key_exists('nofollow', $data)) { |
| 583 |
$existing = get_post_meta($post_id, 'rank_math_robots', true); |
| 584 |
$robots = is_array($existing) ? $existing : []; |
| 585 |
$robots = array_values(array_diff($robots, ['index', 'noindex', 'follow', 'nofollow'])); |
| 586 |
if (array_key_exists('noindex', $data)) { |
| 587 |
$robots[] = $data['noindex'] ? 'noindex' : 'index'; |
| 588 |
} |
| 589 |
if (array_key_exists('nofollow', $data)) { |
| 590 |
$robots[] = $data['nofollow'] ? 'nofollow' : 'follow'; |
| 591 |
} |
| 592 |
update_post_meta($post_id, 'rank_math_robots', array_values(array_unique($robots))); |
| 593 |
} |
| 594 |
|
| 595 |
// Advanced robots: max-* go into rank_math_advanced_robots |
| 596 |
$adv_keys = ['max_snippet', 'max_image_preview', 'max_video_preview']; |
| 597 |
$has_adv = false; |
| 598 |
foreach ($adv_keys as $k) { |
| 599 |
if (array_key_exists($k, $data)) { |
| 600 |
$has_adv = true; |
| 601 |
break; |
| 602 |
} |
| 603 |
} |
| 604 |
if ($has_adv) { |
| 605 |
$existing_adv = get_post_meta($post_id, 'rank_math_advanced_robots', true); |
| 606 |
$adv = is_array($existing_adv) ? $existing_adv : []; |
| 607 |
if (array_key_exists('max_snippet', $data) && $data['max_snippet'] !== null) { |
| 608 |
$val = (int) $data['max_snippet']; |
| 609 |
$adv['max-snippet'] = 'max-snippet:' . $val; |
| 610 |
} |
| 611 |
if (array_key_exists('max_image_preview', $data) && $data['max_image_preview'] !== null) { |
| 612 |
$allowed = ['none', 'standard', 'large']; |
| 613 |
if (in_array($data['max_image_preview'], $allowed, true)) { |
| 614 |
$adv['max-image-preview'] = 'max-image-preview:' . $data['max_image_preview']; |
| 615 |
} |
| 616 |
} |
| 617 |
if (array_key_exists('max_video_preview', $data) && $data['max_video_preview'] !== null) { |
| 618 |
$val = (int) $data['max_video_preview']; |
| 619 |
$adv['max-video-preview'] = 'max-video-preview:' . $val; |
| 620 |
} |
| 621 |
if (!empty($adv)) { |
| 622 |
update_post_meta($post_id, 'rank_math_advanced_robots', $adv); |
| 623 |
} |
| 624 |
} |
| 625 |
|
| 626 |
// Sync noarchive/nosnippet/noimageindex into rank_math_robots |
| 627 |
if (array_key_exists('noarchive', $data) || array_key_exists('nosnippet', $data) || array_key_exists('noimageindex', $data)) { |
| 628 |
$existing = get_post_meta($post_id, 'rank_math_robots', true); |
| 629 |
$robots = is_array($existing) ? $existing : []; |
| 630 |
foreach (['noarchive', 'nosnippet', 'noimageindex'] as $dir) { |
| 631 |
if (!array_key_exists($dir, $data)) { |
| 632 |
continue; |
| 633 |
} |
| 634 |
$robots = array_values(array_diff($robots, [$dir])); |
| 635 |
if (!empty($data[$dir])) { |
| 636 |
$robots[] = $dir; |
| 637 |
} |
| 638 |
} |
| 639 |
update_post_meta($post_id, 'rank_math_robots', array_values(array_unique($robots))); |
| 640 |
} |
| 641 |
|
| 642 |
// OG |
| 643 |
if (!empty($data['og_title'])) { |
| 644 |
update_post_meta($post_id, 'rank_math_facebook_title', $data['og_title']); |
| 645 |
} |
| 646 |
if (!empty($data['og_desc'])) { |
| 647 |
update_post_meta($post_id, 'rank_math_facebook_description', $data['og_desc']); |
| 648 |
} |
| 649 |
if (!empty($data['og_image'])) { |
| 650 |
update_post_meta($post_id, 'rank_math_facebook_image', esc_url_raw($data['og_image'])); |
| 651 |
$img_id = attachment_url_to_postid($data['og_image']); |
| 652 |
if ($img_id) { |
| 653 |
update_post_meta($post_id, 'rank_math_facebook_image_id', $img_id); |
| 654 |
} |
| 655 |
} |
| 656 |
|
| 657 |
// Twitter |
| 658 |
if (!empty($data['twitter_title'])) { |
| 659 |
update_post_meta($post_id, 'rank_math_twitter_title', $data['twitter_title']); |
| 660 |
} |
| 661 |
if (!empty($data['twitter_desc'])) { |
| 662 |
update_post_meta($post_id, 'rank_math_twitter_description', $data['twitter_desc']); |
| 663 |
} |
| 664 |
if (!empty($data['twitter_card'])) { |
| 665 |
$valid_cards = ['summary', 'summary_large_image', 'app', 'player']; |
| 666 |
if (in_array($data['twitter_card'], $valid_cards, true)) { |
| 667 |
update_post_meta($post_id, 'rank_math_twitter_card_type', $data['twitter_card']); |
| 668 |
} |
| 669 |
} |
| 670 |
|
| 671 |
// Canonical, focus keyword, breadcrumb |
| 672 |
if (!empty($data['canonical'])) { |
| 673 |
update_post_meta($post_id, 'rank_math_canonical_url', esc_url_raw($data['canonical'])); |
| 674 |
} |
| 675 |
if (!empty($data['focus_keyword'])) { |
| 676 |
update_post_meta($post_id, 'rank_math_focus_keyword', $data['focus_keyword']); |
| 677 |
} |
| 678 |
if (!empty($data['breadcrumb_title'])) { |
| 679 |
update_post_meta($post_id, 'rank_math_breadcrumb_title', $data['breadcrumb_title']); |
| 680 |
} |
| 681 |
|
| 682 |
return true; |
| 683 |
} |
| 684 |
|
| 685 |
/** |
| 686 |
* Mirror canonical data into the AIOSEO wp_aioseo_posts custom table. |
| 687 |
* |
| 688 |
* @param int $post_id Post ID. |
| 689 |
* @param array $data Canonical key/value pairs. |
| 690 |
* @return bool True when the row was written, false when the table is |
| 691 |
* missing or the write failed. |
| 692 |
*/ |
| 693 |
private function sync_aioseo($post_id, array $data) { |
| 694 |
global $wpdb; |
| 695 |
|
| 696 |
$table = $wpdb->prefix . 'aioseo_posts'; |
| 697 |
|
| 698 |
// Bail if the AIOSEO post table does not exist (plugin not initialised). |
| 699 |
$table_exists = $wpdb->get_var($wpdb->prepare('SHOW TABLES LIKE %s', $table)); |
| 700 |
if ($table_exists !== $table) { |
| 701 |
return false; |
| 702 |
} |
| 703 |
|
| 704 |
$row = []; |
| 705 |
|
| 706 |
if (!empty($data['title'])) { |
| 707 |
$row['title'] = sanitize_text_field($data['title']); |
| 708 |
} |
| 709 |
if (!empty($data['desc'])) { |
| 710 |
$row['description'] = sanitize_text_field($data['desc']); |
| 711 |
} |
| 712 |
if (!empty($data['og_title'])) { |
| 713 |
$row['og_title'] = sanitize_text_field($data['og_title']); |
| 714 |
} |
| 715 |
if (!empty($data['og_desc'])) { |
| 716 |
$row['og_description'] = sanitize_text_field($data['og_desc']); |
| 717 |
} |
| 718 |
if (!empty($data['og_image'])) { |
| 719 |
$row['og_image_type'] = 'custom'; |
| 720 |
$row['og_image_custom_url'] = esc_url_raw($data['og_image']); |
| 721 |
} |
| 722 |
if (!empty($data['twitter_title'])) { |
| 723 |
$row['twitter_title'] = sanitize_text_field($data['twitter_title']); |
| 724 |
} |
| 725 |
if (!empty($data['twitter_desc'])) { |
| 726 |
$row['twitter_description'] = sanitize_text_field($data['twitter_desc']); |
| 727 |
} |
| 728 |
if (!empty($data['twitter_card'])) { |
| 729 |
$valid_cards = ['default', 'summary', 'summary_large_image', 'player', 'app']; |
| 730 |
if (in_array($data['twitter_card'], $valid_cards, true)) { |
| 731 |
$row['twitter_card'] = $data['twitter_card']; |
| 732 |
} |
| 733 |
} |
| 734 |
if (!empty($data['canonical'])) { |
| 735 |
$row['canonical_url'] = esc_url_raw($data['canonical']); |
| 736 |
} |
| 737 |
|
| 738 |
// focus keyword as keyphrases JSON |
| 739 |
if (!empty($data['focus_keyword'])) { |
| 740 |
$row['keyphrases'] = wp_json_encode([ |
| 741 |
'focus' => [ |
| 742 |
'keyphrase' => sanitize_text_field($data['focus_keyword']), |
| 743 |
'score' => 0, |
| 744 |
'analysis' => new \stdClass(), |
| 745 |
], |
| 746 |
'additional' => [], |
| 747 |
]); |
| 748 |
} |
| 749 |
|
| 750 |
// Robots |
| 751 |
$has_robots = false; |
| 752 |
foreach (['noindex', 'nofollow', 'noarchive', 'nosnippet', 'noimageindex', 'max_snippet', 'max_image_preview', 'max_video_preview'] as $k) { |
| 753 |
if (array_key_exists($k, $data)) { |
| 754 |
$has_robots = true; |
| 755 |
break; |
| 756 |
} |
| 757 |
} |
| 758 |
if ($has_robots) { |
| 759 |
$row['robots_default'] = 0; |
| 760 |
if (array_key_exists('noindex', $data)) { |
| 761 |
$row['robots_noindex'] = $data['noindex'] ? 1 : 0; |
| 762 |
} |
| 763 |
if (array_key_exists('nofollow', $data)) { |
| 764 |
$row['robots_nofollow'] = $data['nofollow'] ? 1 : 0; |
| 765 |
} |
| 766 |
if (array_key_exists('noarchive', $data)) { |
| 767 |
$row['robots_noarchive'] = !empty($data['noarchive']) ? 1 : 0; |
| 768 |
} |
| 769 |
if (array_key_exists('nosnippet', $data)) { |
| 770 |
$row['robots_nosnippet'] = !empty($data['nosnippet']) ? 1 : 0; |
| 771 |
} |
| 772 |
if (array_key_exists('noimageindex', $data)) { |
| 773 |
$row['robots_noimageindex'] = !empty($data['noimageindex']) ? 1 : 0; |
| 774 |
} |
| 775 |
if (array_key_exists('max_snippet', $data) && $data['max_snippet'] !== null) { |
| 776 |
$row['robots_max_snippet'] = (int) $data['max_snippet']; |
| 777 |
} |
| 778 |
if (array_key_exists('max_video_preview', $data) && $data['max_video_preview'] !== null) { |
| 779 |
$row['robots_max_videopreview'] = (int) $data['max_video_preview']; |
| 780 |
} |
| 781 |
if (array_key_exists('max_image_preview', $data) && $data['max_image_preview'] !== null) { |
| 782 |
$allowed = ['none', 'standard', 'large']; |
| 783 |
if (in_array($data['max_image_preview'], $allowed, true)) { |
| 784 |
$row['robots_max_imagepreview'] = $data['max_image_preview']; |
| 785 |
} |
| 786 |
} |
| 787 |
} |
| 788 |
|
| 789 |
if (empty($row)) { |
| 790 |
return false; |
| 791 |
} |
| 792 |
|
| 793 |
$row['updated'] = current_time('mysql'); |
| 794 |
|
| 795 |
$existing_id = $wpdb->get_var($wpdb->prepare( |
| 796 |
"SELECT id FROM {$table} WHERE post_id = %d", |
| 797 |
$post_id |
| 798 |
)); |
| 799 |
|
| 800 |
if ($existing_id) { |
| 801 |
return $wpdb->update($table, $row, ['post_id' => $post_id]) !== false; |
| 802 |
} |
| 803 |
|
| 804 |
// New row -- must include all NOT NULL columns with no defaults |
| 805 |
$row['post_id'] = $post_id; |
| 806 |
$row['created'] = current_time('mysql'); |
| 807 |
$robot_defaults = [ |
| 808 |
'robots_default' => isset($row['robots_noindex']) ? 0 : 1, |
| 809 |
'robots_noindex' => 0, |
| 810 |
'robots_nofollow' => 0, |
| 811 |
'robots_noarchive' => 0, |
| 812 |
'robots_nosnippet' => 0, |
| 813 |
'robots_noimageindex' => 0, |
| 814 |
'robots_noodp' => 0, |
| 815 |
'robots_notranslate' => 0, |
| 816 |
]; |
| 817 |
$row = array_merge($robot_defaults, $row); |
| 818 |
|
| 819 |
return $wpdb->insert($table, $row) !== false; |
| 820 |
} |
| 821 |
|
| 822 |
// ------------------------------------------------------------------ |
| 823 |
// Two-way sync: sidebar JSON ↔ legacy meta boxes |
| 824 |
// ------------------------------------------------------------------ |
| 825 |
|
| 826 |
/** |
| 827 |
* Mirror _metasync_robots_advanced JSON → legacy meta box keys. |
| 828 |
* |
| 829 |
* Called when the sidebar writes the JSON key so the classic-editor |
| 830 |
* meta boxes reflect the same values. |
| 831 |
* |
| 832 |
* @param int $post_id Post ID. |
| 833 |
* @param string $json_value Raw JSON string from _metasync_robots_advanced. |
| 834 |
*/ |
| 835 |
private function sync_to_legacy_meta($post_id, $json_value) { |
| 836 |
static $syncing_legacy = []; |
| 837 |
if (!empty($syncing_legacy[$post_id])) { |
| 838 |
return; |
| 839 |
} |
| 840 |
$syncing_legacy[$post_id] = true; |
| 841 |
// Block sync_legacy_to_json from running while we write legacy keys |
| 842 |
$this->syncing_json_to_legacy[$post_id] = true; |
| 843 |
|
| 844 |
try { |
| 845 |
$robots = is_string($json_value) ? json_decode($json_value, true) : $json_value; |
| 846 |
if (!is_array($robots)) { |
| 847 |
return; |
| 848 |
} |
| 849 |
|
| 850 |
// Build metasync_common_robots array |
| 851 |
$common = get_post_meta($post_id, 'metasync_common_robots', true); |
| 852 |
if (!is_array($common)) { |
| 853 |
$common = []; |
| 854 |
} |
| 855 |
foreach (['nofollow', 'noarchive', 'nosnippet', 'noimageindex'] as $dir) { |
| 856 |
if (!empty($robots[$dir])) { |
| 857 |
$common[$dir] = $dir; |
| 858 |
} else { |
| 859 |
unset($common[$dir]); |
| 860 |
} |
| 861 |
} |
| 862 |
if (!empty($common)) { |
| 863 |
update_post_meta($post_id, 'metasync_common_robots', $common); |
| 864 |
} else { |
| 865 |
delete_post_meta($post_id, 'metasync_common_robots'); |
| 866 |
} |
| 867 |
|
| 868 |
// Build metasync_advance_robots array — skip default values to keep legacy clean |
| 869 |
$adv = []; |
| 870 |
if (isset($robots['max_snippet']) && $robots['max_snippet'] !== null && (int) $robots['max_snippet'] !== -1) { |
| 871 |
$adv['max-snippet'] = ['enable' => '1', 'length' => (string) $robots['max_snippet']]; |
| 872 |
} |
| 873 |
if (isset($robots['max_image_preview']) && $robots['max_image_preview'] !== null && $robots['max_image_preview'] !== 'large') { |
| 874 |
$adv['max-image-preview'] = ['enable' => '1', 'length' => (string) $robots['max_image_preview']]; |
| 875 |
} |
| 876 |
if (isset($robots['max_video_preview']) && $robots['max_video_preview'] !== null && (int) $robots['max_video_preview'] !== -1) { |
| 877 |
$adv['max-video-preview'] = ['enable' => '1', 'length' => (string) $robots['max_video_preview']]; |
| 878 |
} |
| 879 |
if (!empty($adv)) { |
| 880 |
update_post_meta($post_id, 'metasync_advance_robots', $adv); |
| 881 |
} else { |
| 882 |
delete_post_meta($post_id, 'metasync_advance_robots'); |
| 883 |
} |
| 884 |
} finally { |
| 885 |
unset($syncing_legacy[$post_id]); |
| 886 |
unset($this->syncing_json_to_legacy[$post_id]); |
| 887 |
} |
| 888 |
} |
| 889 |
|
| 890 |
/** |
| 891 |
* Rebuild _metasync_robots_advanced JSON from legacy meta box keys. |
| 892 |
* |
| 893 |
* Called when the classic-editor meta boxes save metasync_common_robots |
| 894 |
* or metasync_advance_robots so the sidebar JSON stays in sync. |
| 895 |
* |
| 896 |
* @param int $post_id Post ID. |
| 897 |
*/ |
| 898 |
private function sync_legacy_to_json($post_id) { |
| 899 |
static $syncing_json = []; |
| 900 |
// If sync_to_legacy_meta is currently running, skip — it already wrote the correct JSON |
| 901 |
if (!empty($this->syncing_json_to_legacy[$post_id])) { |
| 902 |
return; |
| 903 |
} |
| 904 |
if (!empty($syncing_json[$post_id])) { |
| 905 |
return; |
| 906 |
} |
| 907 |
$syncing_json[$post_id] = true; |
| 908 |
|
| 909 |
try { |
| 910 |
$common = get_post_meta($post_id, 'metasync_common_robots', true); |
| 911 |
if (!is_array($common)) { |
| 912 |
$common = []; |
| 913 |
} |
| 914 |
$adv = get_post_meta($post_id, 'metasync_advance_robots', true); |
| 915 |
if (!is_array($adv)) { |
| 916 |
$adv = []; |
| 917 |
} |
| 918 |
|
| 919 |
$json = []; |
| 920 |
|
| 921 |
// Boolean directives from common_robots |
| 922 |
foreach (['nofollow', 'noarchive', 'nosnippet', 'noimageindex'] as $dir) { |
| 923 |
$json[$dir] = !empty($common[$dir]); |
| 924 |
} |
| 925 |
|
| 926 |
// max-* directives from advance_robots |
| 927 |
if (!empty($adv['max-snippet']['enable'])) { |
| 928 |
$json['max_snippet'] = isset($adv['max-snippet']['length']) ? (int) $adv['max-snippet']['length'] : -1; |
| 929 |
} |
| 930 |
if (!empty($adv['max-image-preview']['enable'])) { |
| 931 |
$json['max_image_preview'] = isset($adv['max-image-preview']['length']) ? (string) $adv['max-image-preview']['length'] : 'large'; |
| 932 |
} |
| 933 |
if (!empty($adv['max-video-preview']['enable'])) { |
| 934 |
$json['max_video_preview'] = isset($adv['max-video-preview']['length']) ? (int) $adv['max-video-preview']['length'] : -1; |
| 935 |
} |
| 936 |
|
| 937 |
// Only write if there's something meaningful |
| 938 |
$has_value = false; |
| 939 |
foreach ($json as $v) { |
| 940 |
if ($v !== false && $v !== null) { |
| 941 |
$has_value = true; |
| 942 |
break; |
| 943 |
} |
| 944 |
} |
| 945 |
|
| 946 |
if ($has_value) { |
| 947 |
update_post_meta($post_id, '_metasync_robots_advanced', wp_json_encode($json)); |
| 948 |
} else { |
| 949 |
delete_post_meta($post_id, '_metasync_robots_advanced'); |
| 950 |
} |
| 951 |
} finally { |
| 952 |
unset($syncing_json[$post_id]); |
| 953 |
} |
| 954 |
} |
| 955 |
} |
| 956 |
|