PluginProbe
Search Atlas SEO – OTTO AI SEO Automation for WordPress / 2.7.2
Search Atlas SEO – OTTO AI SEO Automation for WordPress v2.7.2
2.7.1 2.7.2 2.7.0 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 All 141 releases
← All changes | includes/metasync-helpers.php +271 -10 2.6.23 → 2.7.2 View file →
@@ -63,9 +63,9 @@
63 63 *
64 64 * @return bool True when the current request is a WP core scrape self-check.
65 65 */
66 66 function metasync_is_scrape_request(){
67 - return isset($_GET['wp_scrape_key']) || isset($_GET['wp_scrape_nonce']);
67 + return isset($_GET['wp_scrape_key']) || isset($_GET['wp_scrape_nonce']); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only front-end flag to skip buffering
68 68 }
69 69 }
70 70
71 71 if (!function_exists('metasync_get_custom_page_exclusion_meta_query')) {
@@ -71,30 +71,255 @@
71 71 if (!function_exists('metasync_get_custom_page_exclusion_meta_query')) {
72 72 /**
73 73 * Build a WP_Query meta_query fragment that excludes custom/LPS pages.
74 74 *
75 - * When AND-combined with any query, this drops posts whose
76 - * _metasync_is_custom_html_page marker is set to '1' while keeping all
77 - * WordPress-managed posts (where the key is absent or set to anything else).
75 + * Mirrors the sibling predicate metasync_is_custom_or_lps_page(): a post is
76 + * excluded when ANY of the three markers is set — _metasync_is_custom_html_page
77 + * = '1', or a non-empty _metasync_lps_import / _metasync_created_via_api.
78 + * Checking only the first marker let LPS placeholder pages leak into SEO
79 + * audits as real pages.
78 80 *
79 81 * @return array meta_query fragment.
80 82 */
81 83 function metasync_get_custom_page_exclusion_meta_query(){
82 84 return array(
83 - 'relation' => 'OR',
85 + 'relation' => 'AND',
84 86 array(
85 - 'key' => '_metasync_is_custom_html_page',
86 - 'compare' => 'NOT EXISTS',
87 + 'relation' => 'OR',
88 + array(
89 + 'key' => '_metasync_is_custom_html_page',
90 + 'compare' => 'NOT EXISTS',
91 + ),
92 + array(
93 + 'key' => '_metasync_is_custom_html_page',
94 + 'value' => '1',
95 + 'compare' => '!=',
96 + ),
87 97 ),
88 98 array(
89 - 'key' => '_metasync_is_custom_html_page',
90 - 'value' => '1',
91 - 'compare' => '!=',
99 + 'relation' => 'OR',
100 + array(
101 + 'key' => '_metasync_lps_import',
102 + 'compare' => 'NOT EXISTS',
103 + ),
104 + // IN ('', '0') mirrors empty() semantics for stored strings.
105 + array(
106 + 'key' => '_metasync_lps_import',
107 + 'value' => array('', '0'),
108 + 'compare' => 'IN',
109 + ),
92 110 ),
111 + array(
112 + 'relation' => 'OR',
113 + array(
114 + 'key' => '_metasync_created_via_api',
115 + 'compare' => 'NOT EXISTS',
116 + ),
117 + array(
118 + 'key' => '_metasync_created_via_api',
119 + 'value' => array('', '0'),
120 + 'compare' => 'IN',
121 + ),
122 + ),
93 123 );
94 124 }
95 125 }
96 126
127 +if (!function_exists('metasync_get_custom_page_exclusion_where_sql')) {
128 + /**
129 + * Build a prepared SQL WHERE fragment that excludes custom/LPS pages.
130 + *
131 + * Produces three AND-ed NOT EXISTS correlated anti-joins against wp_postmeta.
132 + * Unlike WP core's meta_query translation (which generates unanchored
133 + * post_id-only LEFT JOINs multiplied across meta rows), this WHERE clause
134 + * checks only the 3 specific marker keys and avoids joining postmeta.
135 + *
136 + * Semantics match metasync_is_custom_or_lps_page() and
137 + * metasync_get_custom_page_exclusion_meta_query() exactly:
138 + * - Excluded if _metasync_is_custom_html_page = '1'
139 + * - Excluded if _metasync_lps_import NOT IN ('', '0')
140 + * - Excluded if _metasync_created_via_api NOT IN ('', '0')
141 + *
142 + * @return string Prepared SQL fragment starting with ' AND ...'
143 + */
144 + function metasync_get_custom_page_exclusion_where_sql() {
145 + global $wpdb;
146 +
147 + if (!isset($wpdb) || !is_object($wpdb) || empty($wpdb->postmeta) || empty($wpdb->posts)) {
148 + return '';
149 + }
150 +
151 + return $wpdb->prepare(
152 + " AND NOT EXISTS (\n"
153 + . "\tSELECT 1 FROM {$wpdb->postmeta} AS _ms_ex_html\n"
154 + . "\tWHERE _ms_ex_html.post_id = {$wpdb->posts}.ID\n"
155 + . "\t\tAND _ms_ex_html.meta_key = %s\n"
156 + . "\t\tAND _ms_ex_html.meta_value = %s\n"
157 + . ")\n"
158 + . "AND NOT EXISTS (\n"
159 + . "\tSELECT 1 FROM {$wpdb->postmeta} AS _ms_ex_lps\n"
160 + . "\tWHERE _ms_ex_lps.post_id = {$wpdb->posts}.ID\n"
161 + . "\t\tAND _ms_ex_lps.meta_key = %s\n"
162 + . "\t\tAND _ms_ex_lps.meta_value NOT IN ('', '0')\n"
163 + . ")\n"
164 + . "AND NOT EXISTS (\n"
165 + . "\tSELECT 1 FROM {$wpdb->postmeta} AS _ms_ex_api\n"
166 + . "\tWHERE _ms_ex_api.post_id = {$wpdb->posts}.ID\n"
167 + . "\t\tAND _ms_ex_api.meta_key = %s\n"
168 + . "\t\tAND _ms_ex_api.meta_value NOT IN ('', '0')\n"
169 + . ")",
170 + '_metasync_is_custom_html_page',
171 + '1',
172 + '_metasync_lps_import',
173 + '_metasync_created_via_api'
174 + );
175 + }
176 +}
177 +
178 +if (!function_exists('metasync_filter_exclude_custom_pages_where')) {
179 + /**
180 + * WP_Query posts_where filter callback to exclude custom/LPS pages.
181 + *
182 + * Checks for the sentinel query variable 'metasync_exclude_custom_pages'.
183 + *
184 + * @param string $where Existing WHERE clause.
185 + * @param WP_Query $query Current query.
186 + * @return string Modified WHERE clause.
187 + */
188 + function metasync_filter_exclude_custom_pages_where($where, $query) {
189 + if (empty($query->get('metasync_exclude_custom_pages'))) {
190 + return $where;
191 + }
192 +
193 + return $where . metasync_get_custom_page_exclusion_where_sql();
194 + }
195 +}
196 +
197 +if (!function_exists('metasync_get_sitemap_post_type_objects')) {
198 + /**
199 + * Return the post types that may be selected by a sitemap picker.
200 + *
201 + * Sitemap settings are shared by the general, news, and video sitemap
202 + * screens. Keep their visibility rules in one helper so the three pickers
203 + * cannot drift apart when a page builder or WordPress adds an internal
204 + * post type.
205 + *
206 + * @return array<int, WP_Post_Type> Viewable public post type objects.
207 + */
208 + function metasync_get_sitemap_post_type_objects(): array
209 + {
210 + $default_excluded_post_types = [
211 + 'attachment',
212 + 'revision',
213 + 'nav_menu_item',
214 + 'elementor_library',
215 + 'elementor-hf',
216 + 'e-floating-buttons',
217 + 'bricks_template',
218 + 'ct_template',
219 + 'oxy_user_library',
220 + 'brizy-template',
221 + 'fusion_template',
222 + 'fusion_tb_section',
223 + 'ae_global_templates',
224 + 'custom_css',
225 + 'customize_changeset',
226 + 'oembed_cache',
227 + 'user_request',
228 + 'wp_block',
229 + 'wp_template',
230 + 'wp_template_part',
231 + 'wp_global_styles',
232 + 'wp_navigation',
233 + 'acf-field-group',
234 + 'acf-field',
235 + 'fl-builder-template',
236 + 'fl-theme-layout',
237 + 'wpr_mega_menu',
238 + 'wpr_templates',
239 + ];
240 +
241 + /**
242 + * Allow integrations to remove additional internal post types from
243 + * all sitemap post-type pickers.
244 + *
245 + * @param string[] $excluded_post_types Post type slugs to exclude.
246 + */
247 + /** @var mixed $filtered_excluded_post_types */
248 + $filtered_excluded_post_types = apply_filters(
249 + 'metasync_sitemap_excluded_post_types',
250 + $default_excluded_post_types
251 + );
252 + $excluded_post_types = is_array($filtered_excluded_post_types)
253 + ? $filtered_excluded_post_types
254 + : $default_excluded_post_types;
255 +
256 + /**
257 + * Allow integrations and tests to adjust the public post-type objects
258 + * before the common exclusion and viewability rules are applied.
259 + *
260 + * @param array<string, WP_Post_Type> $post_types Public post types.
261 + */
262 + /** @var mixed $filtered_post_types */
263 + $filtered_post_types = apply_filters(
264 + 'metasync_sitemap_post_types',
265 + get_post_types(['public' => true], 'objects')
266 + );
267 + $post_types = is_array($filtered_post_types) ? $filtered_post_types : [];
268 + $filtered = [];
269 + foreach ($post_types as $post_type) {
270 + if (!is_object($post_type) || empty($post_type->name)) {
271 + continue;
272 + }
273 + if (in_array($post_type->name, $excluded_post_types, true)) {
274 + continue;
275 + }
276 + if (!is_post_type_viewable($post_type)) {
277 + continue;
278 + }
279 + $filtered[] = $post_type;
280 + }
281 +
282 + return $filtered;
283 + }
284 +}
285 +
286 +if (!function_exists('metasync_repair_scheme_prefixed_media_id')) {
287 + /**
288 + * Recover a media-library attachment ID from the corrupted value the legacy
289 + * logo sanitizer wrote.
290 + *
291 + * The legacy save path ran the Local Business logo through
292 + * sanitize_url() (= esc_url_raw()), which prepends a scheme to any value
293 + * that has none. A stored attachment ID such as "45589" therefore became
294 + * "http://45589". That shape is unambiguous — "https?://" followed by
295 + * digits only, with no dot and no path, which can never be a resolvable
296 + * host — so the digits are exactly the original attachment ID and
297 + * recovering them is mechanically safe.
298 + *
299 + * Values that are not of that shape are returned untouched, so this is
300 + * safe to call on every read of a media setting: a real URL (which always
301 + * carries a dot in its host) and a plain attachment ID both pass through.
302 + *
303 + * Used by the schema output, the admin preview, and the one-time database
304 + * repair migration, so all three can never disagree.
305 + *
306 + * @param mixed $value Stored media setting (attachment ID, URL, or corrupted value).
307 + * @return string|int The attachment ID when $value was corrupted, $value otherwise.
308 + */
309 + function metasync_repair_scheme_prefixed_media_id($value){
310 + if (is_numeric($value) || !is_string($value)) {
311 + return $value;
312 + }
313 +
314 + if (!preg_match('/^https?:\/\/(\d+)\/?$/i', trim($value), $matches)) {
315 + return $value;
316 + }
317 +
318 + return (int) $matches[1];
319 + }
320 +}
321 +
97 322 if (!function_exists('metasync_discard_buffered_output')) {
98 323 /**
99 324 * Discard any pending output buffers before emitting a machine-readable body.
100 325 *
@@ -263,5 +488,41 @@
263 488 }
264 489
265 490 return $result;
266 491 }
492 +}
493 +
494 +/**
495 + * Is headless mode active right now?
496 + *
497 + * Single skew-proof entry point for every caller that needs the headless flag.
498 + *
499 + * Metasync_Headless_Config is reached ONLY through the committed Composer
500 + * classmap — nothing require_once's it, and vendor/composer/autoload_psr4.php
501 + * is empty, so there is no fallback lookup path. A partially updated install
502 + * (an interrupted upgrade, a half-finished file sync, or stale opcache bytecode
503 + * for a single file) can therefore leave a newer caller beside a missing or
504 + * older copy of the class, and an unguarded static call fatals. Several callers
505 + * run on wp/wp_head, where that fatal is a white screen on every page view
506 + * rather than a degraded feature.
507 + *
508 + * This file IS explicitly require_once'd by metasync.php, so it cannot itself
509 + * be the unresolvable one — which is why the guard lives here rather than in a
510 + * class that would have the same problem it is trying to solve.
511 + *
512 + * Treating an unresolvable class as "not headless" is the safe default: the
513 + * site keeps rendering exactly as it did before headless mode existed.
514 + *
515 + * @return bool True only when the class resolves AND reports headless active.
516 + */
517 +function metasync_headless_is_active()
518 +{
519 + // class_exists() is redundant to static analysis — the class ships in this
520 + // same plugin, so PHPStan proves the call always resolves. It is kept for
521 + // the upgrade window described above, which exists only at runtime.
522 + // @phpstan-ignore-next-line function.alreadyNarrowedType
523 + if (!class_exists('Metasync_Headless_Config')) {
524 + return false;
525 + }
526 +
527 + return (bool) Metasync_Headless_Config::is_active();
267 528 }