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

Search Atlas SEO – OTTO AI SEO Automation for WordPress, version 2.6.22. 150 lines.

- Page: https://pluginprobe.com/plugins/metasync/2.6.22/code/includes/metasync-helpers.php
- Raw: https://pluginprobe.com/plugins/metasync/2.6.22/raw/includes/metasync-helpers.php
- Modified: 2026-07-30T00:14:02+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.6.22/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']);
    }
}

if (!function_exists('metasync_get_custom_page_exclusion_meta_query')) {
    /**
     * Build a WP_Query meta_query fragment that excludes custom/LPS pages.
     *
     * When AND-combined with any query, this drops posts whose
     * _metasync_is_custom_html_page marker is set to '1' while keeping all
     * WordPress-managed posts (where the key is absent or set to anything else).
     *
     * @return array meta_query fragment.
     */
    function metasync_get_custom_page_exclusion_meta_query(){
        return array(
            'relation' => 'OR',
            array(
                'key'     => '_metasync_is_custom_html_page',
                'compare' => 'NOT EXISTS',
            ),
            array(
                'key'     => '_metasync_is_custom_html_page',
                'value'   => '1',
                'compare' => '!=',
            ),
        );
    }
}

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;
    }
}

```
