PluginProbe
Search Atlas SEO – OTTO AI SEO Automation for WordPress / 2.6.20
Search Atlas SEO – OTTO AI SEO Automation for WordPress v2.6.20
2.6.26 2.6.25 2.6.24 2.6.23 2.6.22 2.6.21 2.6.20 2.6.19 2.6.18 2.6.17 2.6.16 2.6.15 2.6.14 2.6.13 2.6.12 2.6.11 2.6.10 2.6.9 2.6.8 2.6.7 2.6.6 2.6.5 2.6.4 2.6.3 2.5.23 All 138 releases
metasync / includes / class-metasync-term-plugin-sync.php

class-metasync-term-plugin-sync.php in Search Atlas SEO – OTTO AI SEO Automation for WordPress 2.6.20, at includes/class-metasync-term-plugin-sync.php

329 lines 12.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Term-Level SEO Plugin Sync
4 *
5 * Mirrors MetaSync term meta (`_metasync_*`) into the active third-party
6 * SEO plugins' term storage (Yoast, Rank Math, AIOSEO) so that category,
7 * tag, and custom-taxonomy archive pages render MetaSync-managed values
8 * regardless of which plugin is actually rendering the archive.
9 *
10 * @package MetaSync
11 * @subpackage MetaSync/includes
12 * @since 2.8.24
13 */
14
15 if (!defined('ABSPATH')) {
16 exit;
17 }
18
19 class Metasync_Term_Plugin_Sync {
20
21 /**
22 * Singleton instance.
23 *
24 * @var self|null
25 */
26 private static $instance = null;
27
28 /**
29 * Get singleton instance.
30 *
31 * @return self
32 */
33 public static function get_instance() {
34 if (self::$instance === null) {
35 self::$instance = new self();
36 }
37 return self::$instance;
38 }
39
40 /**
41 * Propagate MetaSync term meta to every active SEO plugin.
42 *
43 * Canonical $data keys recognised by this method:
44 * title, desc, og_title, og_desc, og_image,
45 * twitter_title, twitter_desc, canonical, noindex
46 *
47 * Callers may include any subset; empty values are skipped by each
48 * plugin-specific sync method so a partial update does not clobber
49 * fields that were not passed in.
50 *
51 * @param int $term_id Term ID.
52 * @param string $taxonomy Taxonomy slug (unused by the plugins but kept
53 * in the signature for future use / logging).
54 * @param array $data Canonical key/value pairs.
55 * @return array Results keyed by plugin: ['yoast'=>bool,'rankmath'=>bool,'aioseo'=>bool].
56 */
57 public function sync_term($term_id, $taxonomy, array $data) {
58 // Explicit static recursion guard — prevents re-entrant calls for the same
59 // term (e.g. if a term_meta hook triggers another sync_term() call).
60 static $syncing = [];
61 if (!empty($syncing[$term_id])) {
62 return [];
63 }
64 $syncing[$term_id] = true;
65
66 try {
67 $results = [];
68
69 if ($term_id <= 0 || empty($data)) {
70 return $results;
71 }
72
73 // Never mirror a corrupted or non-URL canonical into third-party
74 // storage — a nested-array value cast with (string) becomes the
75 // literal "Array" and propagates into Yoast/RankMath/AIOSEO.
76 if (array_key_exists('canonical', $data)) {
77 $data['canonical'] = Metasync_Canonical_Sanitizer::sanitize($data['canonical']);
78 if ($data['canonical'] === '') {
79 unset($data['canonical']);
80 }
81 }
82
83 if ($this->is_yoast_active()) {
84 $results['yoast'] = $this->sync_yoast((int) $term_id, $taxonomy, $data);
85 }
86
87 if ($this->is_rankmath_active()) {
88 $results['rankmath'] = $this->sync_rankmath((int) $term_id, $data);
89 }
90
91 if ($this->is_aioseo_active()) {
92 $results['aioseo'] = $this->sync_aioseo((int) $term_id, $data);
93 }
94
95 return $results;
96 } finally {
97 unset($syncing[$term_id]);
98 }
99 }
100
101 // ------------------------------------------------------------------
102 // Plugin detectors
103 // ------------------------------------------------------------------
104
105 /**
106 * Check whether Yoast SEO (free or premium) is active.
107 *
108 * @return bool
109 */
110 private function is_yoast_active() {
111 $this->ensure_plugin_api();
112 return is_plugin_active('wordpress-seo/wp-seo.php')
113 || is_plugin_active('wordpress-seo-premium/wp-seo-premium.php');
114 }
115
116 /**
117 * Check whether Rank Math SEO is active.
118 *
119 * @return bool
120 */
121 private function is_rankmath_active() {
122 $this->ensure_plugin_api();
123 return is_plugin_active('seo-by-rank-math/rank-math.php')
124 || is_plugin_active('seo-by-rankmath/rank-math.php');
125 }
126
127 /**
128 * Check whether AIOSEO (free or pro) is active.
129 *
130 * @return bool
131 */
132 private function is_aioseo_active() {
133 $this->ensure_plugin_api();
134 return is_plugin_active('all-in-one-seo-pack/all_in_one_seo_pack.php')
135 || is_plugin_active('all-in-one-seo-pack-pro/all_in_one_seo_pack.php');
136 }
137
138 /**
139 * Ensure is_plugin_active() is loaded on the frontend.
140 */
141 private function ensure_plugin_api() {
142 if (!function_exists('is_plugin_active')) {
143 require_once ABSPATH . 'wp-admin/includes/plugin.php';
144 }
145 }
146
147 // ------------------------------------------------------------------
148 // Per-plugin sync
149 // ------------------------------------------------------------------
150
151 /**
152 * Mirror canonical data into Yoast term storage.
153 *
154 * Yoast stores taxonomy term SEO data in the `wpseo_taxonomy_meta` option
155 * (wp_options), NOT in wp_termmeta. The `WPSEO_Taxonomy_Meta::set_value()`
156 * API is the correct way to write to this storage.
157 *
158 * @param int $term_id Term ID.
159 * @param string $taxonomy Taxonomy slug (required by Yoast API).
160 * @param array $data Canonical key/value pairs.
161 * @return bool Always true once dispatch completes.
162 */
163 private function sync_yoast($term_id, $taxonomy, array $data) {
164 if (!class_exists('WPSEO_Taxonomy_Meta')) {
165 return false;
166 }
167
168 $field_map = [
169 'title' => 'wpseo_title',
170 'desc' => 'wpseo_desc',
171 'og_title' => 'wpseo_opengraph-title',
172 'og_desc' => 'wpseo_opengraph-description',
173 'og_image' => 'wpseo_opengraph-image',
174 'twitter_title' => 'wpseo_twitter-title',
175 'twitter_desc' => 'wpseo_twitter-description',
176 'canonical' => 'wpseo_canonical',
177 ];
178
179 $meta_values = [];
180
181 foreach ($field_map as $canonical_key => $yoast_key) {
182 if (array_key_exists($canonical_key, $data) && $data[$canonical_key] !== '') {
183 $meta_values[$yoast_key] = (string) $data[$canonical_key];
184 }
185 }
186
187 if (array_key_exists('noindex', $data)) {
188 $is_noindex = ($data['noindex'] === 'noindex' || $data['noindex'] === true || $data['noindex'] === 1 || $data['noindex'] === '1');
189 $meta_values['wpseo_noindex'] = $is_noindex ? 'noindex' : 'default';
190 }
191
192 if (!empty($meta_values)) {
193 // Yoast's set_values() replaces the entire term entry. Merge our
194 // new values with the existing stored values so we don't clobber
195 // previously synced fields.
196 $existing = WPSEO_Taxonomy_Meta::get_term_meta($term_id, $taxonomy);
197 if (is_array($existing)) {
198 $meta_values = array_merge($existing, $meta_values);
199 }
200 WPSEO_Taxonomy_Meta::set_values($term_id, $taxonomy, $meta_values);
201
202 // Rebuild the Yoast indexable so the frontend and sitemaps render
203 // the updated values. Yoast's Indexable_Term_Watcher listens on
204 // `edited_term`; we fire it to trigger the rebuild.
205 $term_obj = get_term($term_id, $taxonomy);
206 $tt_id = ($term_obj && !is_wp_error($term_obj)) ? (int) $term_obj->term_taxonomy_id : 0;
207 do_action('edited_term', $term_id, $tt_id, $taxonomy);
208 }
209
210 return true;
211 }
212
213 /**
214 * Mirror canonical data into Rank Math term meta (wp_termmeta).
215 *
216 * @param int $term_id Term ID.
217 * @param array $data Canonical key/value pairs.
218 * @return bool Always true once dispatch completes.
219 */
220 private function sync_rankmath($term_id, array $data) {
221 if (array_key_exists('title', $data) && $data['title'] !== '') {
222 update_term_meta($term_id, 'rank_math_title', (string) $data['title']);
223 }
224 if (array_key_exists('desc', $data) && $data['desc'] !== '') {
225 update_term_meta($term_id, 'rank_math_description', (string) $data['desc']);
226 }
227 if (array_key_exists('og_title', $data) && $data['og_title'] !== '') {
228 update_term_meta($term_id, 'rank_math_facebook_title', (string) $data['og_title']);
229 }
230 if (array_key_exists('og_desc', $data) && $data['og_desc'] !== '') {
231 update_term_meta($term_id, 'rank_math_facebook_description', (string) $data['og_desc']);
232 }
233 if (array_key_exists('canonical', $data) && $data['canonical'] !== '') {
234 update_term_meta($term_id, 'rank_math_canonical_url', (string) $data['canonical']);
235 }
236 if (array_key_exists('noindex', $data)) {
237 // Rank Math stores robots directives as a serialized PHP array (e.g. ['noindex']).
238 $is_noindex = ($data['noindex'] === 'noindex' || $data['noindex'] === true || $data['noindex'] === 1 || $data['noindex'] === '1');
239 $robots = $is_noindex ? ['noindex'] : [];
240 update_term_meta($term_id, 'rank_math_robots', $robots);
241 }
242
243 return true;
244 }
245
246 /**
247 * Mirror canonical data into the AIOSEO `wp_aioseo_terms` custom table.
248 *
249 * @param int $term_id Term ID.
250 * @param array $data Canonical key/value pairs.
251 * @return bool True when the row was written, false when the table is
252 * missing or the write failed.
253 */
254 private function sync_aioseo($term_id, array $data) {
255 global $wpdb;
256
257 $table = $wpdb->prefix . 'aioseo_terms';
258
259 // Bail if the AIOSEO term table does not exist (plugin not initialised).
260 $table_exists = $wpdb->get_var($wpdb->prepare('SHOW TABLES LIKE %s', $table));
261 if ($table_exists !== $table) {
262 return false;
263 }
264
265 $row = [];
266 if (array_key_exists('title', $data) && $data['title'] !== '') {
267 $row['title'] = (string) $data['title'];
268 }
269 if (array_key_exists('desc', $data) && $data['desc'] !== '') {
270 $row['description'] = (string) $data['desc'];
271 }
272 if (array_key_exists('og_title', $data) && $data['og_title'] !== '') {
273 $row['og_title'] = (string) $data['og_title'];
274 }
275 if (array_key_exists('og_desc', $data) && $data['og_desc'] !== '') {
276 $row['og_description'] = (string) $data['og_desc'];
277 }
278 if (array_key_exists('canonical', $data) && $data['canonical'] !== '') {
279 $row['canonical_url'] = (string) $data['canonical'];
280 }
281 if (array_key_exists('noindex', $data)) {
282 $is_noindex = ($data['noindex'] === 'noindex' || $data['noindex'] === true || $data['noindex'] === 1 || $data['noindex'] === '1');
283 $row['robots_noindex'] = $is_noindex ? 1 : 0;
284 // When explicitly setting noindex, disable AIOSEO's global-defaults
285 // fallback so the explicit value takes effect.
286 $row['robots_default'] = 0;
287 }
288
289 if (empty($row)) {
290 return false;
291 }
292
293 $row['updated'] = current_time('mysql');
294
295 $existing_id = $wpdb->get_var($wpdb->prepare(
296 "SELECT id FROM {$table} WHERE term_id = %d",
297 $term_id
298 ));
299
300 if ($existing_id) {
301 $updated = $wpdb->update($table, $row, ['term_id' => $term_id]);
302 return $updated !== false;
303 }
304
305 // New row: include term_id, timestamps, and NOT NULL robot defaults.
306 $row['term_id'] = $term_id;
307 $row['created'] = current_time('mysql');
308
309 // AIOSEO's robots_* columns are NOT NULL with no DB default.
310 // Use robots_default=1 so AIOSEO falls back to global settings,
311 // then only override robots_noindex when explicitly set above.
312 $robot_defaults = [
313 'robots_default' => isset($row['robots_noindex']) ? 0 : 1,
314 'robots_noindex' => 0,
315 'robots_noarchive' => 0,
316 'robots_nosnippet' => 0,
317 'robots_nofollow' => 0,
318 'robots_noimageindex' => 0,
319 'robots_noodp' => 0,
320 'robots_notranslate' => 0,
321 ];
322 // Merge defaults first, then $row on top so our noindex value wins.
323 $row = array_merge($robot_defaults, $row);
324
325 $inserted = $wpdb->insert($table, $row);
326 return $inserted !== false;
327 }
328 }
329