| @@ -1,9 +1,8 @@ | ||
| 1 | 1 | <?php |
| 2 | 2 | |
| 3 | 3 | namespace ABlocks\API; |
| 4 | 4 | |
| 5 | -use ABlocks\Helper; | |
| 6 | 5 | use WP_REST_Server; |
| 7 | 6 | use WP_REST_Response; |
| 8 | 7 | |
| 9 | 8 | if ( ! defined( 'ABSPATH' ) ) { |
| @@ -81,12 +80,24 @@ | ||
| 81 | 80 | $term_id = (int) $payload['term_id']; |
| 82 | 81 | $page = (int) ( isset( $payload['page'] ) ? $payload['page'] : 1 ); |
| 83 | 82 | $is_archive = (bool) ( isset( $payload['is_archive'] ) ? $payload['is_archive'] : false ); |
| 84 | 83 | |
| 84 | + // This route is public, so the request may only narrow the loop to a | |
| 85 | + // post type or taxonomy the site already exposes to visitors. sanitize_key | |
| 86 | + // fixes the spelling, not the visibility: without this an anonymous | |
| 87 | + // request could point an archive loop at an internal post type. | |
| 88 | + if ( ! empty( $payload['archive_post_type'] ) && ! is_post_type_viewable( (string) $payload['archive_post_type'] ) ) { | |
| 89 | + return $this->empty_response( $term_id, $post_id, 400 ); | |
| 90 | + } | |
| 91 | + if ( ! empty( $taxonomy ) && ! is_taxonomy_viewable( (string) $taxonomy ) ) { | |
| 92 | + return $this->empty_response( $term_id, $post_id, 400 ); | |
| 93 | + } | |
| 94 | + | |
| 85 | 95 | /* ---------------- ARCHIVE LOGIC (UNCHANGED) ---------------- */ |
| 86 | 96 | |
| 87 | 97 | if ( $is_archive ) { |
| 88 | 98 | |
| 99 | + $template_slug = ''; | |
| 89 | 100 | if ( ! empty( $payload['archive_post_type'] ) ) { |
| 90 | 101 | $template_slug = 'archive-' . $payload['archive_post_type']; |
| 91 | 102 | |
| 92 | 103 | } elseif ( ! empty( $payload['taxonomy'] ) ) { |
| @@ -113,30 +124,31 @@ | ||
| 113 | 124 | ], |
| 114 | 125 | ], |
| 115 | 126 | ]); |
| 116 | 127 | |
| 117 | - $post_content = current( $template_posts )->post_content; | |
| 118 | - $blocks = parse_blocks( $post_content ); | |
| 128 | + $template_post = current( $template_posts ); | |
| 129 | + $blocks = $template_post ? parse_blocks( $template_post->post_content ) : []; | |
| 119 | 130 | |
| 120 | - $block_data = Helper::get_block_attributes_recursive( | |
| 121 | - $loop_builder_block_id, | |
| 122 | - 'ablocks/loop-builder', | |
| 123 | - $blocks | |
| 124 | - ); | |
| 131 | + } else { | |
| 125 | 132 | |
| 126 | - } else { | |
| 133 | + $post = get_post( $post_id ); | |
| 134 | + $blocks = $this->is_post_readable( $post ) ? parse_blocks( $post->post_content ) : []; | |
| 135 | + }//end if | |
| 127 | 136 | |
| 128 | - $block_data = Helper::get_block_attributes( | |
| 129 | - $post_id, | |
| 130 | - $loop_builder_block_id, | |
| 131 | - 'ablocks/loop-builder' | |
| 132 | - ); | |
| 137 | + // The loop may sit inside a synced pattern or template part referenced | |
| 138 | + // from the content, or in the theme template rendered around the post. | |
| 139 | + $loop_builder_block = $this->find_block( $blocks, $loop_builder_block_id, 'ablocks/loop-builder' ); | |
| 140 | + if ( ! $loop_builder_block ) { | |
| 141 | + $loop_builder_block = $this->find_block_in_theme_templates( $loop_builder_block_id, 'ablocks/loop-builder' ); | |
| 142 | + } | |
| 133 | 143 | |
| 134 | - $post = get_post( $post_id ); | |
| 135 | - $post_content = $post->post_content; | |
| 136 | - $blocks = parse_blocks( $post_content ); | |
| 137 | - }//end if | |
| 144 | + if ( ! $loop_builder_block ) { | |
| 145 | + return $this->empty_response( $term_id, $post_id, 404 ); | |
| 146 | + } | |
| 138 | 147 | |
| 148 | + $block_data = [ 'parentAttributes' => $loop_builder_block['attrs'] ]; | |
| 149 | + $blocks = [ $loop_builder_block ]; | |
| 150 | + | |
| 139 | 151 | /* ---------------- QUERY LOGIC (UNCHANGED) ---------------- */ |
| 140 | 152 | |
| 141 | 153 | $query_vars = isset( $block_data['parentAttributes']['query'] ) |
| 142 | 154 | ? $this->convert_to_wp_query_args( $block_data['parentAttributes']['query'] ) |
| @@ -149,9 +161,23 @@ | ||
| 149 | 161 | if ( $is_archive && ! empty( $payload['archive_post_type'] ) ) { |
| 150 | 162 | $query_vars['post_type'] = $payload['archive_post_type']; |
| 151 | 163 | } |
| 152 | 164 | |
| 153 | - $query_vars['posts_per_page'] = $query_vars['posts_per_page'] * $page; | |
| 165 | + // "Load more" asks for page N and receives the first N pages at once, so | |
| 166 | + // the request's page number multiplies the stored page size. It comes | |
| 167 | + // from an anonymous request, so bound the result: at most | |
| 168 | + // `ablocks/loop_builder/max_posts_per_request` posts (never fewer than | |
| 169 | + // one stored page), however large the page number. | |
| 170 | + $per_page = (int) $query_vars['posts_per_page']; | |
| 171 | + $max_posts = (int) apply_filters( 'ablocks/loop_builder/max_posts_per_request', 100, $block_data['parentAttributes'] ); | |
| 172 | + if ( $per_page < 1 ) { | |
| 173 | + // A loop set to show every post still gets the bound. | |
| 174 | + $query_vars['posts_per_page'] = max( 1, $max_posts ); | |
| 175 | + } else { | |
| 176 | + $max_posts = max( $per_page, $max_posts ); | |
| 177 | + $page = max( 1, min( $page, (int) ceil( $max_posts / $per_page ) ) ); | |
| 178 | + $query_vars['posts_per_page'] = min( $per_page * $page, $max_posts ); | |
| 179 | + } | |
| 154 | 180 | |
| 155 | 181 | if ( ! empty( $taxonomy ) && $term_id ) { |
| 156 | 182 | $query_vars['tax_query'] = [ |
| 157 | 183 | [ |
| @@ -162,13 +188,8 @@ | ||
| 162 | 188 | ], |
| 163 | 189 | ]; |
| 164 | 190 | } |
| 165 | 191 | |
| 166 | - $blocks = $this->get_loop_builder_blocks_by_block_id( | |
| 167 | - $blocks, | |
| 168 | - $loop_builder_block_id | |
| 169 | - ); | |
| 170 | - | |
| 171 | 192 | $updated_blocks = $this->update_loop_builder_query( |
| 172 | 193 | $blocks, |
| 173 | 194 | $loop_template_block_id, |
| 174 | 195 | $query_vars, |
| @@ -180,9 +201,16 @@ | ||
| 180 | 201 | foreach ( $updated_blocks as $block ) { |
| 181 | 202 | $html .= serialize_block( $block ); |
| 182 | 203 | } |
| 183 | 204 | |
| 184 | - $rendered = apply_filters( 'the_content', do_blocks( $html ) ); | |
| 205 | + // Render the fragment the way block templates are rendered. Running the | |
| 206 | + // already-rendered markup through the_content re-applied wpautop (stray | |
| 207 | + // <p> tags) and let other plugins append their post-content extras. | |
| 208 | + $rendered = do_shortcode( shortcode_unautop( $html ) ); | |
| 209 | + $rendered = do_blocks( $rendered ); | |
| 210 | + $rendered = wptexturize( $rendered ); | |
| 211 | + $rendered = convert_smilies( $rendered ); | |
| 212 | + $rendered = wp_filter_content_tags( $rendered, 'template' ); | |
| 185 | 213 | |
| 186 | 214 | /* ---------------- REST RESPONSE (ONLY CHANGE) ---------------- */ |
| 187 | 215 | |
| 188 | 216 | return new WP_REST_Response( |
| @@ -195,8 +223,113 @@ | ||
| 195 | 223 | ], |
| 196 | 224 | ], |
| 197 | 225 | 200 |
| 198 | 226 | ); |
| 227 | + } | |
| 228 | + | |
| 229 | + /** | |
| 230 | + * The response for a request that cannot be rendered. | |
| 231 | + */ | |
| 232 | + private function empty_response( $term_id, $post_id, $status ) { | |
| 233 | + return new WP_REST_Response( | |
| 234 | + [ | |
| 235 | + 'success' => false, | |
| 236 | + 'data' => [ | |
| 237 | + 'html' => '', | |
| 238 | + 'term_id' => $term_id, | |
| 239 | + 'post_id' => $post_id, | |
| 240 | + ], | |
| 241 | + ], | |
| 242 | + $status | |
| 243 | + ); | |
| 244 | + } | |
| 245 | + | |
| 246 | + /** | |
| 247 | + * Only published, non-password-protected content is public; anything else | |
| 248 | + * needs the current user to be able to read it. | |
| 249 | + */ | |
| 250 | + private function is_post_readable( $post ) { | |
| 251 | + if ( ! $post instanceof \WP_Post ) { | |
| 252 | + return false; | |
| 253 | + } | |
| 254 | + if ( 'publish' === $post->post_status && ! post_password_required( $post ) ) { | |
| 255 | + return true; | |
| 256 | + } | |
| 257 | + return current_user_can( 'read_post', $post->ID ); | |
| 258 | + } | |
| 259 | + | |
| 260 | + /** | |
| 261 | + * Find a block by name + block_id, following synced pattern references | |
| 262 | + * (core/block) and template parts (core/template-part). | |
| 263 | + */ | |
| 264 | + private function find_block( array $blocks, $block_id, $block_name, array $visited = [] ) { | |
| 265 | + foreach ( $blocks as $block ) { | |
| 266 | + $name = isset( $block['blockName'] ) ? $block['blockName'] : ''; | |
| 267 | + | |
| 268 | + if ( $name === $block_name && ( isset( $block['attrs']['block_id'] ) ? $block['attrs']['block_id'] : '' ) === $block_id ) { | |
| 269 | + return $block; | |
| 270 | + } | |
| 271 | + | |
| 272 | + if ( ! empty( $block['innerBlocks'] ) ) { | |
| 273 | + $found = $this->find_block( $block['innerBlocks'], $block_id, $block_name, $visited ); | |
| 274 | + if ( $found ) { | |
| 275 | + return $found; | |
| 276 | + } | |
| 277 | + } | |
| 278 | + | |
| 279 | + $ref_key = ''; | |
| 280 | + $content = null; | |
| 281 | + if ( 'core/block' === $name && ! empty( $block['attrs']['ref'] ) ) { | |
| 282 | + $ref_key = 'wp_block:' . (int) $block['attrs']['ref']; | |
| 283 | + if ( ! isset( $visited[ $ref_key ] ) ) { | |
| 284 | + $ref_post = get_post( (int) $block['attrs']['ref'] ); | |
| 285 | + if ( $ref_post && 'wp_block' === $ref_post->post_type && $this->is_post_readable( $ref_post ) ) { | |
| 286 | + $content = $ref_post->post_content; | |
| 287 | + } | |
| 288 | + } | |
| 289 | + } elseif ( 'core/template-part' === $name && ! empty( $block['attrs']['slug'] ) ) { | |
| 290 | + $theme = ! empty( $block['attrs']['theme'] ) ? $block['attrs']['theme'] : get_stylesheet(); | |
| 291 | + $ref_key = 'wp_template_part:' . $theme . '//' . $block['attrs']['slug']; | |
| 292 | + if ( ! isset( $visited[ $ref_key ] ) ) { | |
| 293 | + $part = get_block_template( $theme . '//' . $block['attrs']['slug'], 'wp_template_part' ); | |
| 294 | + if ( $part && 'publish' === $part->status ) { | |
| 295 | + $content = $part->content; | |
| 296 | + } | |
| 297 | + } | |
| 298 | + } | |
| 299 | + | |
| 300 | + if ( is_string( $content ) && '' !== $content ) { | |
| 301 | + $visited[ $ref_key ] = true; | |
| 302 | + $found = $this->find_block( parse_blocks( $content ), $block_id, $block_name, $visited ); | |
| 303 | + if ( $found ) { | |
| 304 | + return $found; | |
| 305 | + } | |
| 306 | + } | |
| 307 | + }//end foreach | |
| 308 | + return null; | |
| 309 | + } | |
| 310 | + | |
| 311 | + /** | |
| 312 | + * Fallback for loops placed in the active theme's templates or template | |
| 313 | + * parts rather than in the post content itself. | |
| 314 | + */ | |
| 315 | + private function find_block_in_theme_templates( $block_id, $block_name ) { | |
| 316 | + foreach ( [ 'wp_template', 'wp_template_part' ] as $template_type ) { | |
| 317 | + foreach ( get_block_templates( [], $template_type ) as $template ) { | |
| 318 | + $content = (string) $template->content; | |
| 319 | + if ( | |
| 320 | + 'publish' !== $template->status || | |
| 321 | + ( false === strpos( $content, $block_id ) && false === strpos( $content, '<!-- wp:block ' ) && false === strpos( $content, '<!-- wp:template-part ' ) ) | |
| 322 | + ) { | |
| 323 | + continue; | |
| 324 | + } | |
| 325 | + $found = $this->find_block( parse_blocks( $content ), $block_id, $block_name ); | |
| 326 | + if ( $found ) { | |
| 327 | + return $found; | |
| 328 | + } | |
| 329 | + } | |
| 330 | + } | |
| 331 | + return null; | |
| 199 | 332 | } |
| 200 | 333 | |
| 201 | 334 | public function get_loop_builder_blocks_by_block_id( $blocks, $target_block_id ) { |
| 202 | 335 | foreach ( $blocks as $block ) { |