| @@ -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,130 @@ | ||
| 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 | + | |
| 97 | 197 | if (!function_exists('metasync_get_sitemap_post_type_objects')) { |
| 98 | 198 | /** |
| 99 | 199 | * Return the post types that may be selected by a sitemap picker. |
| 100 | 200 | * |
| @@ -388,5 +488,41 @@ | ||
| 388 | 488 | } |
| 389 | 489 | |
| 390 | 490 | return $result; |
| 391 | 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(); | |
| 392 | 528 | } |