PluginProbe
Search Atlas SEO – OTTO AI SEO Automation for WordPress / 2.6.7
Search Atlas SEO – OTTO AI SEO Automation for WordPress v2.6.7
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.7, at includes/class-metasync-term-plugin-sync.php

319 lines 11.7 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 if ($this->is_yoast_active()) {
74 $results['yoast'] = $this->sync_yoast((int) $term_id, $taxonomy, $data);
75 }
76
77 if ($this->is_rankmath_active()) {
78 $results['rankmath'] = $this->sync_rankmath((int) $term_id, $data);
79 }
80
81 if ($this->is_aioseo_active()) {
82 $results['aioseo'] = $this->sync_aioseo((int) $term_id, $data);
83 }
84
85 return $results;
86 } finally {
87 unset($syncing[$term_id]);
88 }
89 }
90
91 // ------------------------------------------------------------------
92 // Plugin detectors
93 // ------------------------------------------------------------------
94
95 /**
96 * Check whether Yoast SEO (free or premium) is active.
97 *
98 * @return bool
99 */
100 private function is_yoast_active() {
101 $this->ensure_plugin_api();
102 return is_plugin_active('wordpress-seo/wp-seo.php')
103 || is_plugin_active('wordpress-seo-premium/wp-seo-premium.php');
104 }
105
106 /**
107 * Check whether Rank Math SEO is active.
108 *
109 * @return bool
110 */
111 private function is_rankmath_active() {
112 $this->ensure_plugin_api();
113 return is_plugin_active('seo-by-rank-math/rank-math.php')
114 || is_plugin_active('seo-by-rankmath/rank-math.php');
115 }
116
117 /**
118 * Check whether AIOSEO (free or pro) is active.
119 *
120 * @return bool
121 */
122 private function is_aioseo_active() {
123 $this->ensure_plugin_api();
124 return is_plugin_active('all-in-one-seo-pack/all_in_one_seo_pack.php')
125 || is_plugin_active('all-in-one-seo-pack-pro/all_in_one_seo_pack.php');
126 }
127
128 /**
129 * Ensure is_plugin_active() is loaded on the frontend.
130 */
131 private function ensure_plugin_api() {
132 if (!function_exists('is_plugin_active')) {
133 require_once ABSPATH . 'wp-admin/includes/plugin.php';
134 }
135 }
136
137 // ------------------------------------------------------------------
138 // Per-plugin sync
139 // ------------------------------------------------------------------
140
141 /**
142 * Mirror canonical data into Yoast term storage.
143 *
144 * Yoast stores taxonomy term SEO data in the `wpseo_taxonomy_meta` option
145 * (wp_options), NOT in wp_termmeta. The `WPSEO_Taxonomy_Meta::set_value()`
146 * API is the correct way to write to this storage.
147 *
148 * @param int $term_id Term ID.
149 * @param string $taxonomy Taxonomy slug (required by Yoast API).
150 * @param array $data Canonical key/value pairs.
151 * @return bool Always true once dispatch completes.
152 */
153 private function sync_yoast($term_id, $taxonomy, array $data) {
154 if (!class_exists('WPSEO_Taxonomy_Meta')) {
155 return false;
156 }
157
158 $field_map = [
159 'title' => 'wpseo_title',
160 'desc' => 'wpseo_desc',
161 'og_title' => 'wpseo_opengraph-title',
162 'og_desc' => 'wpseo_opengraph-description',
163 'og_image' => 'wpseo_opengraph-image',
164 'twitter_title' => 'wpseo_twitter-title',
165 'twitter_desc' => 'wpseo_twitter-description',
166 'canonical' => 'wpseo_canonical',
167 ];
168
169 $meta_values = [];
170
171 foreach ($field_map as $canonical_key => $yoast_key) {
172 if (array_key_exists($canonical_key, $data) && $data[$canonical_key] !== '') {
173 $meta_values[$yoast_key] = (string) $data[$canonical_key];
174 }
175 }
176
177 if (array_key_exists('noindex', $data)) {
178 $is_noindex = ($data['noindex'] === 'noindex' || $data['noindex'] === true || $data['noindex'] === 1 || $data['noindex'] === '1');
179 $meta_values['wpseo_noindex'] = $is_noindex ? 'noindex' : 'default';
180 }
181
182 if (!empty($meta_values)) {
183 // Yoast's set_values() replaces the entire term entry. Merge our
184 // new values with the existing stored values so we don't clobber
185 // previously synced fields.
186 $existing = WPSEO_Taxonomy_Meta::get_term_meta($term_id, $taxonomy);
187 if (is_array($existing)) {
188 $meta_values = array_merge($existing, $meta_values);
189 }
190 WPSEO_Taxonomy_Meta::set_values($term_id, $taxonomy, $meta_values);
191
192 // Rebuild the Yoast indexable so the frontend and sitemaps render
193 // the updated values. Yoast's Indexable_Term_Watcher listens on
194 // `edited_term`; we fire it to trigger the rebuild.
195 $term_obj = get_term($term_id, $taxonomy);
196 $tt_id = ($term_obj && !is_wp_error($term_obj)) ? (int) $term_obj->term_taxonomy_id : 0;
197 do_action('edited_term', $term_id, $tt_id, $taxonomy);
198 }
199
200 return true;
201 }
202
203 /**
204 * Mirror canonical data into Rank Math term meta (wp_termmeta).
205 *
206 * @param int $term_id Term ID.
207 * @param array $data Canonical key/value pairs.
208 * @return bool Always true once dispatch completes.
209 */
210 private function sync_rankmath($term_id, array $data) {
211 if (array_key_exists('title', $data) && $data['title'] !== '') {
212 update_term_meta($term_id, 'rank_math_title', (string) $data['title']);
213 }
214 if (array_key_exists('desc', $data) && $data['desc'] !== '') {
215 update_term_meta($term_id, 'rank_math_description', (string) $data['desc']);
216 }
217 if (array_key_exists('og_title', $data) && $data['og_title'] !== '') {
218 update_term_meta($term_id, 'rank_math_facebook_title', (string) $data['og_title']);
219 }
220 if (array_key_exists('og_desc', $data) && $data['og_desc'] !== '') {
221 update_term_meta($term_id, 'rank_math_facebook_description', (string) $data['og_desc']);
222 }
223 if (array_key_exists('canonical', $data) && $data['canonical'] !== '') {
224 update_term_meta($term_id, 'rank_math_canonical_url', (string) $data['canonical']);
225 }
226 if (array_key_exists('noindex', $data)) {
227 // Rank Math stores robots directives as a serialized PHP array (e.g. ['noindex']).
228 $is_noindex = ($data['noindex'] === 'noindex' || $data['noindex'] === true || $data['noindex'] === 1 || $data['noindex'] === '1');
229 $robots = $is_noindex ? ['noindex'] : [];
230 update_term_meta($term_id, 'rank_math_robots', $robots);
231 }
232
233 return true;
234 }
235
236 /**
237 * Mirror canonical data into the AIOSEO `wp_aioseo_terms` custom table.
238 *
239 * @param int $term_id Term ID.
240 * @param array $data Canonical key/value pairs.
241 * @return bool True when the row was written, false when the table is
242 * missing or the write failed.
243 */
244 private function sync_aioseo($term_id, array $data) {
245 global $wpdb;
246
247 $table = $wpdb->prefix . 'aioseo_terms';
248
249 // Bail if the AIOSEO term table does not exist (plugin not initialised).
250 $table_exists = $wpdb->get_var($wpdb->prepare('SHOW TABLES LIKE %s', $table));
251 if ($table_exists !== $table) {
252 return false;
253 }
254
255 $row = [];
256 if (array_key_exists('title', $data) && $data['title'] !== '') {
257 $row['title'] = (string) $data['title'];
258 }
259 if (array_key_exists('desc', $data) && $data['desc'] !== '') {
260 $row['description'] = (string) $data['desc'];
261 }
262 if (array_key_exists('og_title', $data) && $data['og_title'] !== '') {
263 $row['og_title'] = (string) $data['og_title'];
264 }
265 if (array_key_exists('og_desc', $data) && $data['og_desc'] !== '') {
266 $row['og_description'] = (string) $data['og_desc'];
267 }
268 if (array_key_exists('canonical', $data) && $data['canonical'] !== '') {
269 $row['canonical_url'] = (string) $data['canonical'];
270 }
271 if (array_key_exists('noindex', $data)) {
272 $is_noindex = ($data['noindex'] === 'noindex' || $data['noindex'] === true || $data['noindex'] === 1 || $data['noindex'] === '1');
273 $row['robots_noindex'] = $is_noindex ? 1 : 0;
274 // When explicitly setting noindex, disable AIOSEO's global-defaults
275 // fallback so the explicit value takes effect.
276 $row['robots_default'] = 0;
277 }
278
279 if (empty($row)) {
280 return false;
281 }
282
283 $row['updated'] = current_time('mysql');
284
285 $existing_id = $wpdb->get_var($wpdb->prepare(
286 "SELECT id FROM {$table} WHERE term_id = %d",
287 $term_id
288 ));
289
290 if ($existing_id) {
291 $updated = $wpdb->update($table, $row, ['term_id' => $term_id]);
292 return $updated !== false;
293 }
294
295 // New row: include term_id, timestamps, and NOT NULL robot defaults.
296 $row['term_id'] = $term_id;
297 $row['created'] = current_time('mysql');
298
299 // AIOSEO's robots_* columns are NOT NULL with no DB default.
300 // Use robots_default=1 so AIOSEO falls back to global settings,
301 // then only override robots_noindex when explicitly set above.
302 $robot_defaults = [
303 'robots_default' => isset($row['robots_noindex']) ? 0 : 1,
304 'robots_noindex' => 0,
305 'robots_noarchive' => 0,
306 'robots_nosnippet' => 0,
307 'robots_nofollow' => 0,
308 'robots_noimageindex' => 0,
309 'robots_noodp' => 0,
310 'robots_notranslate' => 0,
311 ];
312 // Merge defaults first, then $row on top so our noindex value wins.
313 $row = array_merge($robot_defaults, $row);
314
315 $inserted = $wpdb->insert($table, $row);
316 return $inserted !== false;
317 }
318 }
319