| @@ -912,8 +912,17 @@ | ||
| 912 | 912 | if ( $this->invalid_request_query_vars !== null ) { |
| 913 | 913 | return; |
| 914 | 914 | } |
| 915 | 915 | |
| 916 | + // A single-doc request whose main query returned nothing (private or | |
| 917 | + // draft doc for a visitor who can't read it, a doc in another/inactive | |
| 918 | + // WPML language) must stay a 404. Marking it singular here would render | |
| 919 | + // the single template against a null post and expose the doc's title | |
| 920 | + // through the queried object. See betterdocs/betterdocs#173. | |
| 921 | + if ( $this->is_singular_docs_request() && ! $this->main_query_has_posts() ) { | |
| 922 | + return; | |
| 923 | + } | |
| 924 | + | |
| 916 | 925 | // Check if we have doc_category or knowledge_base in query vars |
| 917 | 926 | if ( isset( $wp_query->query_vars['doc_category'] ) && ! empty( $wp_query->query_vars['doc_category'] ) ) { |
| 918 | 927 | // If this is already identified as singular, don't override it |
| 919 | 928 | if ( $wp_query->is_singular() || $wp_query->is_singular ) { |
| @@ -1080,8 +1089,16 @@ | ||
| 1080 | 1089 | if ( $this->invalid_request_query_vars !== null ) { |
| 1081 | 1090 | return $status_header; |
| 1082 | 1091 | } |
| 1083 | 1092 | |
| 1093 | + // A single-doc request whose main query returned nothing is a genuine | |
| 1094 | + // 404 — whatever the slug/term lookups below would find. Checked before | |
| 1095 | + // both overrides, so neither can turn it into a soft-404 200. | |
| 1096 | + // See betterdocs/betterdocs#173. | |
| 1097 | + if ( 404 === (int) $code && $this->is_singular_docs_request() && ! $this->main_query_has_posts() ) { | |
| 1098 | + return $status_header; | |
| 1099 | + } | |
| 1100 | + | |
| 1084 | 1101 | // If a 404 is being sent but the queried object is a valid single docs post, |
| 1085 | 1102 | // override with 200. This guards against false 404s on single docs pages. |
| 1086 | 1103 | if ( $code == 404 && |
| 1087 | 1104 | isset( $wp_query->queried_object ) && |
| @@ -1142,8 +1159,41 @@ | ||
| 1142 | 1159 | return 'HTTP/1.1 200 OK'; |
| 1143 | 1160 | } |
| 1144 | 1161 | |
| 1145 | 1162 | return $status_header; |
| 1163 | + } | |
| 1164 | + | |
| 1165 | + /** | |
| 1166 | + * Whether the main query targets a single doc (`name`, `docs` or `p`), | |
| 1167 | + * as opposed to an archive. | |
| 1168 | + * | |
| 1169 | + * @return bool | |
| 1170 | + */ | |
| 1171 | + protected function is_singular_docs_request() { | |
| 1172 | + global $wp_query; | |
| 1173 | + | |
| 1174 | + if ( ! $wp_query instanceof \WP_Query ) { | |
| 1175 | + return false; | |
| 1176 | + } | |
| 1177 | + | |
| 1178 | + $query_vars = $wp_query->query_vars; | |
| 1179 | + | |
| 1180 | + return ! empty( $query_vars['name'] ) | |
| 1181 | + || ! empty( $query_vars['docs'] ) | |
| 1182 | + || ( isset( $query_vars['p'] ) && (int) $query_vars['p'] > 0 ); | |
| 1183 | + } | |
| 1184 | + | |
| 1185 | + /** | |
| 1186 | + * Whether the main query actually returned posts. Slug and term lookups | |
| 1187 | + * only prove something exists; this is what the visitor can see — it | |
| 1188 | + * already honours post status, capabilities and WPML's language filter. | |
| 1189 | + * | |
| 1190 | + * @return bool | |
| 1191 | + */ | |
| 1192 | + protected function main_query_has_posts() { | |
| 1193 | + global $wp_query; | |
| 1194 | + | |
| 1195 | + return $wp_query instanceof \WP_Query && ! empty( $wp_query->posts ); | |
| 1146 | 1196 | } |
| 1147 | 1197 | |
| 1148 | 1198 | /** |
| 1149 | 1199 | * Ensure tax_query is always initialized as an object |