# metasync/2.7.2/includes/metasync-helpers.php

Search Atlas SEO – OTTO AI SEO Automation for WordPress, version 2.7.2. 529 lines.

- Page: https://pluginprobe.com/plugins/metasync/2.7.2/code/includes/metasync-helpers.php
- Raw: https://pluginprobe.com/plugins/metasync/2.7.2/raw/includes/metasync-helpers.php
- Modified: 2026-09-25T22:31:34+00:00

Line numbers below start at 1. Link to a line or a range by appending a fragment to the
page URL, for example `https://pluginprobe.com/plugins/metasync/2.7.2/code/includes/metasync-helpers.php#L10-L20`.

```php
<?php
/**
 * Shared MetaSync helper functions.
 *
 * Always loaded (in all request contexts: admin, REST, MCP, AJAX) so that the
 * custom/LPS page detection rule lives in exactly one place and every SEO
 * surface applies the same exclusion without copy-paste drift.
 *
 * @package Search_Atlas
 */

if (!defined('ABSPATH')) {
    exit;
}

if (!function_exists('metasync_is_custom_or_lps_page')) {
    /**
     * Determine whether a post is a MetaSync-built custom page that ships its own
     * complete, self-contained SEO (Custom HTML pages and LPS-imported pages).
     *
     * These pages already include a full SEO head (title, meta description, OG,
     * Twitter, JSON-LD schema) generated by LPS, so OTTO must not run on them —
     * otherwise it injects/overwrites SEO from a different/older OTTO project and
     * produces a page with two contradictory SEO identities.
     *
     * @param int $post_id Post ID to inspect.
     * @return bool True when the post carries any custom-page / LPS marker.
     */
    function metasync_is_custom_or_lps_page($post_id){
        $post_id = (int) $post_id;
        if ($post_id <= 0 || !class_exists('Metasync_Custom_Pages')) {
            return false;
        }

        if (get_post_meta($post_id, Metasync_Custom_Pages::META_IS_CUSTOM_HTML_PAGE, true) === '1') {
            return true;
        }
        if (!empty(get_post_meta($post_id, Metasync_Custom_Pages::META_LPS_IMPORT, true))) {
            return true;
        }
        if (!empty(get_post_meta($post_id, Metasync_Custom_Pages::META_CREATED_VIA_API, true))) {
            return true;
        }

        return false;
    }
}

if (!function_exists('metasync_is_scrape_request')) {
    /**
     * Detect WordPress core's internal file-editor "scrape" self-check request.
     *
     * When an admin saves a file in the Plugin/Theme Editor, WP core
     * (wp_edit_theme_plugin_file()) fires a loopback request carrying
     * wp_scrape_key + wp_scrape_nonce to detect whether the edit white-screened
     * the site. For a THEME edit that loopback targets home_url('/') — a
     * front-end GET that is NOT is_admin() — so it slips past OTTO's admin/AJAX/
     * REST guards in metasync_start_otto(). OTTO must never buffer/rewrite it:
     * the SimpleHtmlDom rewrite, and any fatal thrown inside the
     * ob_start() display handler, corrupt the scrape and surface as the
     * misleading "preg_match(): Cannot use output buffering in output buffering
     * display handlers" error.
     *
     * @return bool True when the current request is a WP core scrape self-check.
     */
    function metasync_is_scrape_request(){
        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
    }
}

if (!function_exists('metasync_get_custom_page_exclusion_meta_query')) {
    /**
     * Build a WP_Query meta_query fragment that excludes custom/LPS pages.
     *
     * Mirrors the sibling predicate metasync_is_custom_or_lps_page(): a post is
     * excluded when ANY of the three markers is set — _metasync_is_custom_html_page
     * = '1', or a non-empty _metasync_lps_import / _metasync_created_via_api.
     * Checking only the first marker let LPS placeholder pages leak into SEO
     * audits as real pages.
     *
     * @return array meta_query fragment.
     */
    function metasync_get_custom_page_exclusion_meta_query(){
        return array(
            'relation' => 'AND',
            array(
                'relation' => 'OR',
                array(
                    'key'     => '_metasync_is_custom_html_page',
                    'compare' => 'NOT EXISTS',
                ),
                array(
                    'key'     => '_metasync_is_custom_html_page',
                    'value'   => '1',
                    'compare' => '!=',
                ),
            ),
            array(
                'relation' => 'OR',
                array(
                    'key'     => '_metasync_lps_import',
                    'compare' => 'NOT EXISTS',
                ),
                // IN ('', '0') mirrors empty() semantics for stored strings.
                array(
                    'key'     => '_metasync_lps_import',
                    'value'   => array('', '0'),
                    'compare' => 'IN',
                ),
            ),
            array(
                'relation' => 'OR',
                array(
                    'key'     => '_metasync_created_via_api',
                    'compare' => 'NOT EXISTS',
                ),
                array(
                    'key'     => '_metasync_created_via_api',
                    'value'   => array('', '0'),
                    'compare' => 'IN',
                ),
            ),
        );
    }
}

if (!function_exists('metasync_get_custom_page_exclusion_where_sql')) {
    /**
     * Build a prepared SQL WHERE fragment that excludes custom/LPS pages.
     *
     * Produces three AND-ed NOT EXISTS correlated anti-joins against wp_postmeta.
     * Unlike WP core's meta_query translation (which generates unanchored
     * post_id-only LEFT JOINs multiplied across meta rows), this WHERE clause
     * checks only the 3 specific marker keys and avoids joining postmeta.
     *
     * Semantics match metasync_is_custom_or_lps_page() and
     * metasync_get_custom_page_exclusion_meta_query() exactly:
     * - Excluded if _metasync_is_custom_html_page = '1'
     * - Excluded if _metasync_lps_import NOT IN ('', '0')
     * - Excluded if _metasync_created_via_api NOT IN ('', '0')
     *
     * @return string Prepared SQL fragment starting with ' AND ...'
     */
    function metasync_get_custom_page_exclusion_where_sql() {
        global $wpdb;

        if (!isset($wpdb) || !is_object($wpdb) || empty($wpdb->postmeta) || empty($wpdb->posts)) {
            return '';
        }

        return $wpdb->prepare(
            " AND NOT EXISTS (\n"
            . "\tSELECT 1 FROM {$wpdb->postmeta} AS _ms_ex_html\n"
            . "\tWHERE _ms_ex_html.post_id = {$wpdb->posts}.ID\n"
            . "\t\tAND _ms_ex_html.meta_key = %s\n"
            . "\t\tAND _ms_ex_html.meta_value = %s\n"
            . ")\n"
            . "AND NOT EXISTS (\n"
            . "\tSELECT 1 FROM {$wpdb->postmeta} AS _ms_ex_lps\n"
            . "\tWHERE _ms_ex_lps.post_id = {$wpdb->posts}.ID\n"
            . "\t\tAND _ms_ex_lps.meta_key = %s\n"
            . "\t\tAND _ms_ex_lps.meta_value NOT IN ('', '0')\n"
            . ")\n"
            . "AND NOT EXISTS (\n"
            . "\tSELECT 1 FROM {$wpdb->postmeta} AS _ms_ex_api\n"
            . "\tWHERE _ms_ex_api.post_id = {$wpdb->posts}.ID\n"
            . "\t\tAND _ms_ex_api.meta_key = %s\n"
            . "\t\tAND _ms_ex_api.meta_value NOT IN ('', '0')\n"
            . ")",
            '_metasync_is_custom_html_page',
            '1',
            '_metasync_lps_import',
            '_metasync_created_via_api'
        );
    }
}

if (!function_exists('metasync_filter_exclude_custom_pages_where')) {
    /**
     * WP_Query posts_where filter callback to exclude custom/LPS pages.
     *
     * Checks for the sentinel query variable 'metasync_exclude_custom_pages'.
     *
     * @param string   $where Existing WHERE clause.
     * @param WP_Query $query Current query.
     * @return string Modified WHERE clause.
     */
    function metasync_filter_exclude_custom_pages_where($where, $query) {
        if (empty($query->get('metasync_exclude_custom_pages'))) {
            return $where;
        }

        return $where . metasync_get_custom_page_exclusion_where_sql();
    }
}

if (!function_exists('metasync_get_sitemap_post_type_objects')) {
    /**
     * Return the post types that may be selected by a sitemap picker.
     *
     * Sitemap settings are shared by the general, news, and video sitemap
     * screens. Keep their visibility rules in one helper so the three pickers
     * cannot drift apart when a page builder or WordPress adds an internal
     * post type.
     *
     * @return array<int, WP_Post_Type> Viewable public post type objects.
     */
    function metasync_get_sitemap_post_type_objects(): array
    {
        $default_excluded_post_types = [
            'attachment',
            'revision',
            'nav_menu_item',
            'elementor_library',
            'elementor-hf',
            'e-floating-buttons',
            'bricks_template',
            'ct_template',
            'oxy_user_library',
            'brizy-template',
            'fusion_template',
            'fusion_tb_section',
            'ae_global_templates',
            'custom_css',
            'customize_changeset',
            'oembed_cache',
            'user_request',
            'wp_block',
            'wp_template',
            'wp_template_part',
            'wp_global_styles',
            'wp_navigation',
            'acf-field-group',
            'acf-field',
            'fl-builder-template',
            'fl-theme-layout',
            'wpr_mega_menu',
            'wpr_templates',
        ];

        /**
         * Allow integrations to remove additional internal post types from
         * all sitemap post-type pickers.
         *
         * @param string[] $excluded_post_types Post type slugs to exclude.
         */
        /** @var mixed $filtered_excluded_post_types */
        $filtered_excluded_post_types = apply_filters(
            'metasync_sitemap_excluded_post_types',
            $default_excluded_post_types
        );
        $excluded_post_types = is_array($filtered_excluded_post_types)
            ? $filtered_excluded_post_types
            : $default_excluded_post_types;

        /**
         * Allow integrations and tests to adjust the public post-type objects
         * before the common exclusion and viewability rules are applied.
         *
         * @param array<string, WP_Post_Type> $post_types Public post types.
         */
        /** @var mixed $filtered_post_types */
        $filtered_post_types = apply_filters(
            'metasync_sitemap_post_types',
            get_post_types(['public' => true], 'objects')
        );
        $post_types = is_array($filtered_post_types) ? $filtered_post_types : [];
        $filtered = [];
        foreach ($post_types as $post_type) {
            if (!is_object($post_type) || empty($post_type->name)) {
                continue;
            }
            if (in_array($post_type->name, $excluded_post_types, true)) {
                continue;
            }
            if (!is_post_type_viewable($post_type)) {
                continue;
            }
            $filtered[] = $post_type;
        }

        return $filtered;
    }
}

if (!function_exists('metasync_repair_scheme_prefixed_media_id')) {
    /**
     * Recover a media-library attachment ID from the corrupted value the legacy
     * logo sanitizer wrote.
     *
     * The legacy save path ran the Local Business logo through
     * sanitize_url() (= esc_url_raw()), which prepends a scheme to any value
     * that has none. A stored attachment ID such as "45589" therefore became
     * "http://45589". That shape is unambiguous — "https?://" followed by
     * digits only, with no dot and no path, which can never be a resolvable
     * host — so the digits are exactly the original attachment ID and
     * recovering them is mechanically safe.
     *
     * Values that are not of that shape are returned untouched, so this is
     * safe to call on every read of a media setting: a real URL (which always
     * carries a dot in its host) and a plain attachment ID both pass through.
     *
     * Used by the schema output, the admin preview, and the one-time database
     * repair migration, so all three can never disagree.
     *
     * @param mixed $value Stored media setting (attachment ID, URL, or corrupted value).
     * @return string|int The attachment ID when $value was corrupted, $value otherwise.
     */
    function metasync_repair_scheme_prefixed_media_id($value){
        if (is_numeric($value) || !is_string($value)) {
            return $value;
        }

        if (!preg_match('/^https?:\/\/(\d+)\/?$/i', trim($value), $matches)) {
            return $value;
        }

        return (int) $matches[1];
    }
}

if (!function_exists('metasync_discard_buffered_output')) {
    /**
     * Discard any pending output buffers before emitting a machine-readable body.
     *
     * Endpoints that serve XML/plain text (sitemaps, llms.txt, the IndexNow key
     * file) must start at the very first byte of the response. When another
     * plugin emits output earlier in the request — most commonly a PHP
     * Deprecated/Notice/Warning rendered as HTML because display_errors or
     * WP_DEBUG_DISPLAY is on — that text sits in the output buffer and gets
     * flushed ahead of the `<?xml` declaration, making the response invalid and
     * causing crawlers to reject the whole document.
     *
     * Call immediately before the header()/echo pair. On a healthy request there
     * is nothing buffered and this is a no-op.
     *
     * @return bool True when the output is guaranteed clean; false when stray
     *              bytes could not be discarded (see the two cases below).
     */
    function metasync_discard_buffered_output(){
        // Headers already sent means the buffer was flushed to the client, so the
        // stray bytes are on the wire and cannot be recalled. Discarding buffers
        // now would only drop legitimate content.
        if (headers_sent()) {
            return false;
        }

        while (ob_get_level() > 0) {
            $status = ob_get_status();
            $flags  = isset($status['flags']) ? (int) $status['flags'] : 0;
            $needed = PHP_OUTPUT_HANDLER_CLEANABLE | PHP_OUTPUT_HANDLER_REMOVABLE;

            // Some buffers cannot be discarded at all — zlib.output_compression
            // and handlers started without the cleanable/removable flags. Check
            // first, because calling ob_end_clean() on one emits a PHP notice,
            // which would add to the very corruption this guards against.
            if (($flags & $needed) !== $needed) {
                return false;
            }

            $level_before = ob_get_level();
            ob_end_clean();

            // Only keep going while the level is actually falling. Looping on
            // ob_get_level() alone would spin forever against a buffer that
            // refuses to close.
            if (ob_get_level() >= $level_before) {
                return false;
            }
        }

        return true;
    }
}

if (!function_exists('metasync_escape_json_ld_for_script')) {
    /**
     * Escape a JSON document string for safe embedding inside a
     * `<script type="application/ld+json">` element.
     *
     * A JSON-LD block lives inside the HTML document, so a string value that
     * contains the literal `</script>` (or any `</` that could grow into one)
     * would let the browser close the script element early and parse the rest
     * of the payload as page markup (XSS / page corruption). JSON itself has
     * no closing-tag concept, so the sequence is neutralised by replacing every
     * `</` with `<\/` — `\/` is a valid JSON escape for `/`, so any
     * spec-compliant JSON consumer (Google, validators, OTTO) decodes the value
     * back to identical content.
     *
     * @param string $json An already-encoded JSON document.
     * @return string The escaped JSON, safe to echo inside a script element.
     */
    function metasync_escape_json_ld_for_script($json){
        return str_replace('</', '<\/', (string) $json);
    }
}

if (!function_exists('metasync_safe_json_ld_encode')) {
    /**
     * Encode a JSON-LD document so it is safe to embed inside a
     * `<script type="application/ld+json">` element.
     *
     * Encodes with wp_json_encode() using the caller's flags, then applies
     * metasync_escape_json_ld_for_script() so a literal `</script>` inside any
     * string value cannot break out of the surrounding script block. When no
     * `</` is present the output is byte-identical to wp_json_encode().
     *
     * @param mixed $data  The JSON-LD document to encode.
     * @param int   $flags json_encode bitmask, e.g. JSON_UNESCAPED_SLASHES.
     * @return string|false The safely-encoded JSON, or false on encode failure
     *                     (mirrors wp_json_encode semantics).
     */
    function metasync_safe_json_ld_encode($data, $flags = JSON_UNESCAPED_SLASHES){
        $json = wp_json_encode($data, $flags);

        if (!is_string($json)) {
            return $json;
        }

        return metasync_escape_json_ld_for_script($json);
    }
}

if (!function_exists('metasync_escape_json_ld_blocks_in_html')) {
    /**
     * Escape every JSON-LD script block inside an HTML fragment.
     *
     * Remote HTML fragments (e.g. OTTO's header insertion) arrive as assembled
     * markup, so a JSON string value that contains a literal `</script>` cannot
     * be fixed with a naive non-greedy `<script>...</script>` match — the match
     * would stop at the injected closer and leave the payload live. Instead,
     * for each `application/ld+json` opening tag the content is extended past
     * successive closing tags until it parses as valid JSON: that is the span
     * the producer meant to be script content, so every `</` inside it is
     * neutralised as `<\/` (a valid JSON escape decoding to the same value).
     *
     * Blocks whose content never parses as JSON are left untouched rather than
     * guessing at their extent. Non-JSON-LD script tags are never modified.
     *
     * @param string $html HTML fragment possibly containing JSON-LD blocks.
     * @return string Fragment with JSON-LD payload closers escaped.
     */
    function metasync_escape_json_ld_blocks_in_html($html){
        $html = (string) $html;
        if (stripos($html, 'application/ld+json') === false) {
            return $html;
        }

        preg_match_all('/<script\b[^>]*application\/ld\+json[^>]*>/i', $html, $opens, PREG_OFFSET_CAPTURE);
        if (empty($opens[0])) {
            return $html;
        }
        if (!preg_match_all('/<\/script\s*>/i', $html, $closers, PREG_OFFSET_CAPTURE)) {
            return $html;
        }

        $result        = $html;
        $processed_from = PHP_INT_MAX;

        // Right to left, so escaping a block never invalidates the offsets of
        // the blocks still to be processed.
        for ($i = count($opens[0]) - 1; $i >= 0; $i--) {
            $open          = $opens[0][$i];
            $content_start = $open[1] + strlen($open[0]);
            if ($open[1] >= $processed_from) {
                continue; // already inside an escaped span
            }

            foreach ($closers[0] as $closer) {
                $end = $closer[1];
                if ($end <= $content_start) {
                    continue;
                }

                $candidate = substr($html, $content_start, $end - $content_start);
                json_decode($candidate);
                if (json_last_error() !== JSON_ERROR_NONE) {
                    continue; // keep extending to the next closing tag
                }

                $escaped = str_replace('</', '<\/', $candidate);
                if ($escaped !== $candidate) {
                    $result = substr_replace($result, $escaped, $content_start, $end - $content_start);
                }
                $processed_from = min($processed_from, $content_start);
                break;
            }
        }

        return $result;
    }
}

/**
 * Is headless mode active right now?
 *
 * Single skew-proof entry point for every caller that needs the headless flag.
 *
 * Metasync_Headless_Config is reached ONLY through the committed Composer
 * classmap — nothing require_once's it, and vendor/composer/autoload_psr4.php
 * is empty, so there is no fallback lookup path. A partially updated install
 * (an interrupted upgrade, a half-finished file sync, or stale opcache bytecode
 * for a single file) can therefore leave a newer caller beside a missing or
 * older copy of the class, and an unguarded static call fatals. Several callers
 * run on wp/wp_head, where that fatal is a white screen on every page view
 * rather than a degraded feature.
 *
 * This file IS explicitly require_once'd by metasync.php, so it cannot itself
 * be the unresolvable one — which is why the guard lives here rather than in a
 * class that would have the same problem it is trying to solve.
 *
 * Treating an unresolvable class as "not headless" is the safe default: the
 * site keeps rendering exactly as it did before headless mode existed.
 *
 * @return bool True only when the class resolves AND reports headless active.
 */
function metasync_headless_is_active()
{
    // class_exists() is redundant to static analysis — the class ships in this
    // same plugin, so PHPStan proves the call always resolves. It is kept for
    // the upgrade window described above, which exists only at runtime.
    // @phpstan-ignore-next-line function.alreadyNarrowedType
    if (!class_exists('Metasync_Headless_Config')) {
        return false;
    }

    return (bool) Metasync_Headless_Config::is_active();
}

```
