| @@ -67,37 +67,8 @@ | ||
| 67 | 67 | 'toggle', // Elementor |
| 68 | 68 | 'faq', // Widely used in third-party add-on element names |
| 69 | 69 | ]; |
| 70 | 70 | |
| 71 | - /** | |
| 72 | - * Builder meta keys that never describe the published page. | |
| 73 | - * | |
| 74 | - * `_fl_builder_draft` holds Beaver Builder changes that were never | |
| 75 | - * published, so a FAQ that exists only there is not on the page (#945). | |
| 76 | - * | |
| 77 | - * @since 2.14.2 | |
| 78 | - * @var string[] | |
| 79 | - */ | |
| 80 | - private const UNPUBLISHED_BUILDER_META_KEYS = ['_fl_builder_draft']; | |
| 81 | - | |
| 82 | - /** | |
| 83 | - * Builder layouts that only count while their builder renders the post. | |
| 84 | - * | |
| 85 | - * Elementor and Beaver Builder both keep their stored layout after the | |
| 86 | - * author switches the page back to the block editor, so the layout alone | |
| 87 | - * does not mean the builder renders it. Each key maps to the builder's own | |
| 88 | - * flag and the value that means "on": Elementor's `_elementor_edit_mode` | |
| 89 | - * is `builder`, Beaver's `_fl_builder_enabled` is any non-empty value | |
| 90 | - * (null here). The same tests as FAQ_Content::builder() (#945). | |
| 91 | - * | |
| 92 | - * @since 2.14.2 | |
| 93 | - * @var array<string,array{0:string,1:string|null}> | |
| 94 | - */ | |
| 95 | - private const FLAGGED_BUILDER_LAYOUTS = [ | |
| 96 | - '_elementor_data' => ['_elementor_edit_mode', 'builder'], | |
| 97 | - '_fl_builder_data' => ['_fl_builder_enabled', null], | |
| 98 | - ]; | |
| 99 | - | |
| 100 | 71 | // Check result statuses. |
| 101 | 72 | public const PASSED = 'passed'; |
| 102 | 73 | public const WARNING = 'warning'; |
| 103 | 74 | public const FAILED = 'failed'; |
| @@ -1721,9 +1692,12 @@ | ||
| 1721 | 1692 | } |
| 1722 | 1693 | |
| 1723 | 1694 | $collecting = false; |
| 1724 | 1695 | |
| 1725 | - if ('disallow' === $field && self::disallow_blocks_everything($value)) { | |
| 1696 | + // `/` is the canonical full block; `/*` is the same instruction | |
| 1697 | + // written for a wildcard-aware crawler, and every answer engine on | |
| 1698 | + // the list is one. | |
| 1699 | + if ('disallow' === $field && ('/' === $value || '/*' === $value)) { | |
| 1726 | 1700 | foreach ($current as $agent) { |
| 1727 | 1701 | $groups[$agent] = true; |
| 1728 | 1702 | } |
| 1729 | 1703 | } |
| @@ -1732,46 +1706,8 @@ | ||
| 1732 | 1706 | return $groups; |
| 1733 | 1707 | } |
| 1734 | 1708 | |
| 1735 | 1709 | /** |
| 1736 | - * Whether a `Disallow:` value matches every URL on the site. | |
| 1737 | - * | |
| 1738 | - * `/` is the canonical full block; `/*` is the same instruction written | |
| 1739 | - * for a wildcard-aware crawler, and every answer engine on the list is | |
| 1740 | - * one. A value is a path pattern in which `*` matches any run of | |
| 1741 | - * characters, so a bare `*` (and `**`, `/**`) matches every path too, and | |
| 1742 | - * Google's parser treats it that way. Recognising only `/` and `/*` | |
| 1743 | - * reported a site that blocked everyone with `Disallow: *` as reachable | |
| 1744 | - * by every answer engine (#890). | |
| 1745 | - * | |
| 1746 | - * A trailing `$` anchors the pattern to the end of the URL. After a `*` | |
| 1747 | - * it changes nothing, since "any run of characters, then the end" still | |
| 1748 | - * matches every path, so `*$` and `/*$` are full blocks as well. Without | |
| 1749 | - * a `*` it narrows the match: `/$` is the home page only, and `/*.pdf$` | |
| 1750 | - * is PDFs only, so neither counts. | |
| 1751 | - * | |
| 1752 | - * An empty value means "allow everything" and must never count as a | |
| 1753 | - * block, so it is rejected here rather than left to the caller. | |
| 1754 | - * | |
| 1755 | - * `Allow:` lines are deliberately not weighed against this: a full | |
| 1756 | - * robots.txt evaluator with longest-match precedence is a separate change. | |
| 1757 | - * | |
| 1758 | - * @since 2.14.2 | |
| 1759 | - * @param string $value Trimmed `Disallow:` value, comment already removed. | |
| 1760 | - * @return bool | |
| 1761 | - */ | |
| 1762 | - private static function disallow_blocks_everything(string $value): bool { | |
| 1763 | - if ('' === $value) { | |
| 1764 | - return false; | |
| 1765 | - } | |
| 1766 | - | |
| 1767 | - // Optional leading slash, then either nothing (`/`) or a run of `*` | |
| 1768 | - // with an optional end anchor (`*`, `/*`, `/*$`, `**$`). A `$` with | |
| 1769 | - // no `*` before it never matches here, so `/$` stays a partial block. | |
| 1770 | - return 1 === preg_match('#^/?(?:\*+\$?)?$#', $value); | |
| 1771 | - } | |
| 1772 | - | |
| 1773 | - /** | |
| 1774 | 1710 | * /llms.txt should be published — it is the one file whose entire purpose |
| 1775 | 1711 | * is telling an AI assistant what this site is and what to read. |
| 1776 | 1712 | * |
| 1777 | 1713 | * @since 2.5.0 |
| @@ -1926,10 +1862,10 @@ | ||
| 1926 | 1862 | } |
| 1927 | 1863 | |
| 1928 | 1864 | // Builder trees: ThinkRank's own elements, and the builders' generic |
| 1929 | 1865 | // accordion/toggle/FAQ elements, which are what a non-ThinkRank FAQ |
| 1930 | - // is actually built from. Only layouts the visitor is served count. | |
| 1931 | - $meta_keys = self::rendered_builder_meta_keys(); | |
| 1866 | + // is actually built from. | |
| 1867 | + $meta_keys = self::builder_meta_keys(); | |
| 1932 | 1868 | |
| 1933 | 1869 | if (empty($meta_keys)) { |
| 1934 | 1870 | return false; |
| 1935 | 1871 | } |
| @@ -1947,10 +1883,8 @@ | ||
| 1947 | 1883 | $marker_clauses[] = 'pm.meta_value LIKE %s'; |
| 1948 | 1884 | $marker_values[] = '%' . $wpdb->esc_like($marker) . '%'; |
| 1949 | 1885 | } |
| 1950 | 1886 | |
| 1951 | - [$gate_sql, $gate_values] = self::builder_layout_renders_sql('pm'); | |
| 1952 | - | |
| 1953 | 1887 | // Same exemption: both placeholder runs are sized from fixed lists — |
| 1954 | 1888 | // the builder meta keys and the marker list — and every value is |
| 1955 | 1889 | // prepared. |
| 1956 | 1890 | // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching |
| @@ -1959,12 +1893,11 @@ | ||
| 1959 | 1893 | "SELECT 1 FROM {$wpdb->postmeta} pm |
| 1960 | 1894 | INNER JOIN {$wpdb->posts} p ON p.ID = pm.post_id |
| 1961 | 1895 | WHERE p.post_status = 'publish' |
| 1962 | 1896 | AND pm.meta_key IN ({$key_placeholders}) |
| 1963 | - AND (" . implode(' OR ', $marker_clauses) . ") | |
| 1964 | - {$gate_sql} | |
| 1965 | - LIMIT 1", | |
| 1966 | - ...array_merge($meta_keys, $marker_values, $gate_values) | |
| 1897 | + AND (" . implode(' OR ', $marker_clauses) . ') | |
| 1898 | + LIMIT 1', | |
| 1899 | + ...array_merge($meta_keys, $marker_values) | |
| 1967 | 1900 | ) |
| 1968 | 1901 | ); |
| 1969 | 1902 | // phpcs:enable |
| 1970 | 1903 | |
| @@ -2081,9 +2014,9 @@ | ||
| 2081 | 2014 | $content_clauses[] = 'p.post_content LIKE %s'; |
| 2082 | 2015 | $content_values[] = '%' . $wpdb->esc_like($marker) . '%'; |
| 2083 | 2016 | } |
| 2084 | 2017 | |
| 2085 | - $meta_keys = self::rendered_builder_meta_keys(); | |
| 2018 | + $meta_keys = self::builder_meta_keys(); | |
| 2086 | 2019 | $meta_sql = ''; |
| 2087 | 2020 | $values = $content_values; |
| 2088 | 2021 | |
| 2089 | 2022 | if (!empty($meta_keys)) { |
| @@ -2093,19 +2026,16 @@ | ||
| 2093 | 2026 | $meta_clauses[] = 'pm.meta_value LIKE %s'; |
| 2094 | 2027 | $meta_values[] = '%' . $wpdb->esc_like($marker) . '%'; |
| 2095 | 2028 | } |
| 2096 | 2029 | |
| 2097 | - [$gate_sql, $gate_values] = self::builder_layout_renders_sql('pm'); | |
| 2098 | - | |
| 2099 | 2030 | $key_placeholders = implode(', ', array_fill(0, count($meta_keys), '%s')); |
| 2100 | 2031 | $meta_sql = " OR EXISTS ( |
| 2101 | 2032 | SELECT 1 FROM {$wpdb->postmeta} pm |
| 2102 | 2033 | WHERE pm.post_id = p.ID |
| 2103 | 2034 | AND pm.meta_key IN ({$key_placeholders}) |
| 2104 | - AND (" . implode(' OR ', $meta_clauses) . ") | |
| 2105 | - {$gate_sql} | |
| 2106 | - )"; | |
| 2107 | - $values = array_merge($content_values, $meta_keys, $meta_values, $gate_values); | |
| 2035 | + AND (" . implode(' OR ', $meta_clauses) . ') | |
| 2036 | + )'; | |
| 2037 | + $values = array_merge($content_values, $meta_keys, $meta_values); | |
| 2108 | 2038 | } |
| 2109 | 2039 | |
| 2110 | 2040 | $values[] = self::CONTENT_SAMPLE_SIZE; |
| 2111 | 2041 | |
| @@ -2159,13 +2089,9 @@ | ||
| 2159 | 2089 | return false; |
| 2160 | 2090 | } |
| 2161 | 2091 | |
| 2162 | 2092 | // Elementor stores its tree as JSON, so the widget name appears verbatim. |
| 2163 | - // Read only while Elementor renders the post: the tree survives | |
| 2164 | - // switching the page back to the block editor (#945). | |
| 2165 | - $elementor = self::builder_layout_renders($post_id, '_elementor_data') | |
| 2166 | - ? get_post_meta($post_id, '_elementor_data', true) | |
| 2167 | - : ''; | |
| 2093 | + $elementor = get_post_meta($post_id, '_elementor_data', true); | |
| 2168 | 2094 | if (is_string($elementor) |
| 2169 | 2095 | && (false !== strpos($elementor, '"' . self::ANSWER_FAQ_NAME . '"') |
| 2170 | 2096 | || false !== strpos($elementor, '"' . self::ANSWER_HOWTO_NAME . '"'))) { |
| 2171 | 2097 | return true; |
| @@ -2177,12 +2103,9 @@ | ||
| 2177 | 2103 | return true; |
| 2178 | 2104 | } |
| 2179 | 2105 | |
| 2180 | 2106 | // Beaver Builder keeps its layout in postmeta as a map of node objects. |
| 2181 | - // Read only while Beaver renders the post, for the same reason (#945). | |
| 2182 | - $layout = self::builder_layout_renders($post_id, '_fl_builder_data') | |
| 2183 | - ? get_post_meta($post_id, '_fl_builder_data', true) | |
| 2184 | - : []; | |
| 2107 | + $layout = get_post_meta($post_id, '_fl_builder_data', true); | |
| 2185 | 2108 | if (is_array($layout)) { |
| 2186 | 2109 | foreach ($layout as $node) { |
| 2187 | 2110 | $settings = is_object($node) ? ($node->settings ?? null) : ($node['settings'] ?? null); |
| 2188 | 2111 | $settings = is_object($settings) ? get_object_vars($settings) : $settings; |
| @@ -2199,9 +2122,9 @@ | ||
| 2199 | 2122 | // while the page was publishing exactly that (#686). Builder_Content |
| 2200 | 2123 | // already knows every key involved — Oxygen 6 is Breakdance under the |
| 2201 | 2124 | // hood, and older releases used two other keys — so ask it rather than |
| 2202 | 2125 | // keeping a second list that can drift. |
| 2203 | - foreach (self::rendered_builder_meta_keys() as $meta_key) { | |
| 2126 | + foreach (self::builder_meta_keys() as $meta_key) { | |
| 2204 | 2127 | if ('_fl_builder_data' === $meta_key || '_elementor_data' === $meta_key) { |
| 2205 | 2128 | continue; // Handled above, in their own storage shapes. |
| 2206 | 2129 | } |
| 2207 | 2130 | |
| @@ -2234,88 +2157,8 @@ | ||
| 2234 | 2157 | require_once $file; |
| 2235 | 2158 | } |
| 2236 | 2159 | |
| 2237 | 2160 | return Builder_Content::builder_meta_keys(); |
| 2238 | - } | |
| 2239 | - | |
| 2240 | - /** | |
| 2241 | - * Builder meta keys that can hold the published page. | |
| 2242 | - * | |
| 2243 | - * {@see builder_meta_keys()} without the unpublished draft keys, which | |
| 2244 | - * Builder_Content lists because the word-count index watches them, not | |
| 2245 | - * because they are ever served (#945). | |
| 2246 | - * | |
| 2247 | - * @since 2.14.2 | |
| 2248 | - * @return string[] | |
| 2249 | - */ | |
| 2250 | - private static function rendered_builder_meta_keys(): array { | |
| 2251 | - return array_values(array_diff(self::builder_meta_keys(), self::UNPUBLISHED_BUILDER_META_KEYS)); | |
| 2252 | - } | |
| 2253 | - | |
| 2254 | - /** | |
| 2255 | - * Whether a builder layout key holds what the visitor is served. | |
| 2256 | - * | |
| 2257 | - * The answer-content readers' guard for #945. Builder_Content gains the | |
| 2258 | - * same guards for its resolver in #910, and once that lands this can ask | |
| 2259 | - * it instead; until then it reads the builders' own flags, exactly as | |
| 2260 | - * FAQ_Content::builder() does. | |
| 2261 | - * | |
| 2262 | - * @since 2.14.2 | |
| 2263 | - * @param int $post_id Post being inspected. | |
| 2264 | - * @param string $meta_key Builder meta key about to be read. | |
| 2265 | - * @return bool | |
| 2266 | - */ | |
| 2267 | - private static function builder_layout_renders(int $post_id, string $meta_key): bool { | |
| 2268 | - if (in_array($meta_key, self::UNPUBLISHED_BUILDER_META_KEYS, true)) { | |
| 2269 | - return false; | |
| 2270 | - } | |
| 2271 | - | |
| 2272 | - if (!isset(self::FLAGGED_BUILDER_LAYOUTS[$meta_key])) { | |
| 2273 | - return true; | |
| 2274 | - } | |
| 2275 | - | |
| 2276 | - [$flag, $on] = self::FLAGGED_BUILDER_LAYOUTS[$meta_key]; | |
| 2277 | - $value = get_post_meta($post_id, $flag, true); | |
| 2278 | - | |
| 2279 | - return null === $on ? !empty($value) : $on === (string) $value; | |
| 2280 | - } | |
| 2281 | - | |
| 2282 | - /** | |
| 2283 | - * {@see builder_layout_renders()} as an SQL condition on a postmeta row. | |
| 2284 | - * | |
| 2285 | - * The site-wide queries match markers in SQL with nothing confirming each | |
| 2286 | - * row in PHP afterwards, so the flag test has to happen there too. Rows | |
| 2287 | - * under any other key pass untouched. `!empty()` in PHP is false for '' and | |
| 2288 | - * '0', which is what the "any non-empty value" branch excludes. | |
| 2289 | - * | |
| 2290 | - * @since 2.14.2 | |
| 2291 | - * @param string $alias Alias of the postmeta row being tested. | |
| 2292 | - * @return array{0:string,1:array<int,string>} ` AND (...)` clause and its values. | |
| 2293 | - */ | |
| 2294 | - private static function builder_layout_renders_sql(string $alias): array { | |
| 2295 | - global $wpdb; | |
| 2296 | - | |
| 2297 | - $flagged = array_keys(self::FLAGGED_BUILDER_LAYOUTS); | |
| 2298 | - $clauses = [$alias . '.meta_key NOT IN (' . implode(', ', array_fill(0, count($flagged), '%s')) . ')']; | |
| 2299 | - $values = $flagged; | |
| 2300 | - | |
| 2301 | - foreach (self::FLAGGED_BUILDER_LAYOUTS as $meta_key => [$flag, $on]) { | |
| 2302 | - $test = null === $on ? "flag.meta_value NOT IN ('', '0')" : 'flag.meta_value = %s'; | |
| 2303 | - $clauses[] = "({$alias}.meta_key = %s AND EXISTS ( | |
| 2304 | - SELECT 1 FROM {$wpdb->postmeta} flag | |
| 2305 | - WHERE flag.post_id = {$alias}.post_id | |
| 2306 | - AND flag.meta_key = %s | |
| 2307 | - AND {$test} | |
| 2308 | - ))"; | |
| 2309 | - $values[] = $meta_key; | |
| 2310 | - $values[] = $flag; | |
| 2311 | - | |
| 2312 | - if (null !== $on) { | |
| 2313 | - $values[] = $on; | |
| 2314 | - } | |
| 2315 | - } | |
| 2316 | - | |
| 2317 | - return [' AND (' . implode(' OR ', $clauses) . ')', $values]; | |
| 2318 | 2161 | } |
| 2319 | 2162 | |
| 2320 | 2163 | /** |
| 2321 | 2164 | * Whether a stored builder blob names a ThinkRank FAQ or How-To. |