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