PluginProbe
aBlocks – Gutenberg Blocks, User Dashboard Builder, Popup Builder, Form Builder & Animation Builder / 2.13.1
aBlocks – Gutenberg Blocks, User Dashboard Builder, Popup Builder, Form Builder & Animation Builder v2.13.1
2.13.0 2.13.1 2.12.0 2.11.1 2.11.0 2.10.0 2.9.0 2.7.4 2.7.5 2.7.6 2.7.7 2.8.0 2.8.1 2.9.1 trunk 1.0 1.0-beta1 1.0-beta2 1.0-beta3 1.0.1 1.0.2 1.0.3 1.1.0 1.1.1 1.1.2 All 80 releases
← All changes | includes/api/loop-builder-controller.php +461 -0 2.7.42.13.1 View file →
@@ -1,0 +1,461 @@
1 +<?php
2 +
3 +namespace ABlocks\API;
4 +
5 +use WP_REST_Server;
6 +use WP_REST_Response;
7 +
8 +if ( ! defined( 'ABSPATH' ) ) {
9 + exit;
10 +}
11 +
12 +class LoopBuilderController {
13 +
14 + public function __construct() {
15 + add_action( 'rest_api_init', [ $this, 'register_routes' ] );
16 + }
17 +
18 + public function register_routes() {
19 +
20 + register_rest_route(
21 + ABLOCKS_REST_NAMESPACE,
22 + '/loop-builder',
23 + [
24 + 'methods' => WP_REST_Server::READABLE,
25 + 'callback' => [ $this, 'render_loop_builder' ],
26 + 'permission_callback' => '__return_true',
27 + 'args' => [
28 + 'loop_builder_block_id' => [
29 + 'required' => true,
30 + 'type' => 'string',
31 + 'sanitize_callback' => 'sanitize_text_field',
32 + ],
33 + 'loop_template_block_id' => [
34 + 'required' => true,
35 + 'type' => 'string',
36 + 'sanitize_callback' => 'sanitize_text_field',
37 + ],
38 + 'taxonomy' => [
39 + 'required' => false,
40 + 'type' => 'string',
41 + 'sanitize_callback' => 'sanitize_key',
42 + ],
43 + 'post_id' => [
44 + 'required' => false,
45 + 'type' => 'integer',
46 + 'sanitize_callback' => 'absint',
47 + ],
48 + 'term_id' => [
49 + 'required' => false,
50 + 'type' => 'integer',
51 + 'sanitize_callback' => 'absint',
52 + ],
53 + 'page' => [
54 + 'required' => false,
55 + 'type' => 'integer',
56 + 'default' => 1,
57 + 'sanitize_callback' => 'absint',
58 + ],
59 + 'is_archive' => [
60 + 'required' => false,
61 + 'type' => 'boolean',
62 + 'default' => false,
63 + ],
64 + 'archive_post_type' => [
65 + 'required' => false,
66 + 'type' => 'string',
67 + 'sanitize_callback' => 'sanitize_key',
68 + ],
69 + ],
70 + ]
71 + );
72 + }
73 +
74 + public function render_loop_builder( $payload ) {
75 +
76 + $post_id = $payload['post_id'];
77 + $loop_builder_block_id = $payload['loop_builder_block_id'];
78 + $loop_template_block_id = $payload['loop_template_block_id'];
79 + $taxonomy = $payload['taxonomy'];
80 + $term_id = (int) $payload['term_id'];
81 + $page = (int) ( isset( $payload['page'] ) ? $payload['page'] : 1 );
82 + $is_archive = (bool) ( isset( $payload['is_archive'] ) ? $payload['is_archive'] : false );
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 +
95 + /* ---------------- ARCHIVE LOGIC (UNCHANGED) ---------------- */
96 +
97 + if ( $is_archive ) {
98 +
99 + $template_slug = '';
100 + if ( ! empty( $payload['archive_post_type'] ) ) {
101 + $template_slug = 'archive-' . $payload['archive_post_type'];
102 +
103 + } elseif ( ! empty( $payload['taxonomy'] ) ) {
104 +
105 + if ( ! empty( $payload['term_slug'] ) ) {
106 + $template_slug = 'archive-' . $payload['taxonomy'] . '-' . $payload['term_slug'];
107 + } else {
108 + $template_slug = 'archive-' . $payload['taxonomy'];
109 + }
110 + }
111 +
112 + $theme_slug = wp_get_theme()->get_stylesheet();
113 +
114 + $template_posts = get_posts([
115 + 'post_type' => 'wp_template',
116 + 'post_status' => 'publish',
117 + 'numberposts' => 1,
118 + 'name' => $template_slug,
119 + 'tax_query' => [
120 + [
121 + 'taxonomy' => 'wp_theme',
122 + 'field' => 'slug',
123 + 'terms' => $theme_slug,
124 + ],
125 + ],
126 + ]);
127 +
128 + $template_post = current( $template_posts );
129 + $blocks = $template_post ? parse_blocks( $template_post->post_content ) : [];
130 +
131 + } else {
132 +
133 + $post = get_post( $post_id );
134 + $blocks = $this->is_post_readable( $post ) ? parse_blocks( $post->post_content ) : [];
135 + }//end if
136 +
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 + }
143 +
144 + if ( ! $loop_builder_block ) {
145 + return $this->empty_response( $term_id, $post_id, 404 );
146 + }
147 +
148 + $block_data = [ 'parentAttributes' => $loop_builder_block['attrs'] ];
149 + $blocks = [ $loop_builder_block ];
150 +
151 + /* ---------------- QUERY LOGIC (UNCHANGED) ---------------- */
152 +
153 + $query_vars = isset( $block_data['parentAttributes']['query'] )
154 + ? $this->convert_to_wp_query_args( $block_data['parentAttributes']['query'] )
155 + : [
156 + 'post_type' => 'post',
157 + 'post_status' => 'publish',
158 + 'posts_per_page' => get_option( 'posts_per_page' ),
159 + ];
160 +
161 + if ( $is_archive && ! empty( $payload['archive_post_type'] ) ) {
162 + $query_vars['post_type'] = $payload['archive_post_type'];
163 + }
164 +
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 + }
180 +
181 + if ( ! empty( $taxonomy ) && $term_id ) {
182 + $query_vars['tax_query'] = [
183 + [
184 + 'taxonomy' => $taxonomy,
185 + 'field' => 'term_id',
186 + 'terms' => [ $term_id ],
187 + 'operator' => 'IN',
188 + ],
189 + ];
190 + }
191 +
192 + $updated_blocks = $this->update_loop_builder_query(
193 + $blocks,
194 + $loop_template_block_id,
195 + $query_vars,
196 + $term_id
197 + );
198 +
199 + $html = '';
200 +
201 + foreach ( $updated_blocks as $block ) {
202 + $html .= serialize_block( $block );
203 + }
204 +
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' );
213 +
214 + /* ---------------- REST RESPONSE (ONLY CHANGE) ---------------- */
215 +
216 + return new WP_REST_Response(
217 + [
218 + 'success' => true,
219 + 'data' => [
220 + 'html' => $rendered,
221 + 'term_id' => $term_id,
222 + 'post_id' => $post_id,
223 + ],
224 + ],
225 + 200
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;
332 + }
333 +
334 + public function get_loop_builder_blocks_by_block_id( $blocks, $target_block_id ) {
335 + foreach ( $blocks as $block ) {
336 + if ( ! empty( $block['attrs']['block_id'] ) && $block['attrs']['block_id'] === $target_block_id ) {
337 + return [ $block ];
338 + }
339 + if ( ! empty( $block['innerBlocks'] ) ) {
340 + $found = $this->get_loop_builder_blocks_by_block_id( $block['innerBlocks'], $target_block_id );
341 + if ( ! empty( $found ) ) {
342 + return $found;
343 + }
344 + }
345 + }
346 + return [];
347 + }
348 +
349 + public function update_loop_builder_query( $blocks, $loop_template_block_id, $query_vars, $term_id ) {
350 + foreach ( $blocks as &$block ) {
351 + // perfectly not worked if multiple loop used
352 + // !empty($block['attrs']['block_id']) && $block['attrs']['block_id'] === $loop_template_block_id
353 +
354 + if ( ! empty( $block['blockName'] ) && $block['blockName'] === 'ablocks/loop-template' ) {
355 + if ( ! isset( $block['attrs']['query'] ) ) {
356 + $block['attrs']['query'] = [];
357 + }
358 + // Merge new query vars
359 + $block['attrs']['query'] = array_merge( $block['attrs']['query'], $query_vars );
360 + }
361 +
362 + if ( 'ablocks/loop-filter' === $block['blockName'] ) {
363 + $block['attrs']['active_term_id'] = $term_id;
364 + }
365 +
366 + // Recurse into inner blocks
367 + if ( ! empty( $block['innerBlocks'] ) ) {
368 + $block['innerBlocks'] = $this->update_loop_builder_query( $block['innerBlocks'], $loop_template_block_id, $query_vars, $term_id );
369 + }
370 + }//end foreach
371 + return $blocks;
372 + }
373 +
374 + public function convert_to_wp_query_args( array $input ): array {
375 + $args = [
376 + 'post_status' => 'publish',
377 + ];
378 +
379 + // Basic pagination
380 + $args['posts_per_page'] = isset( $input['perPage'] ) ? (int) $input['perPage'] : get_option( 'posts_per_page' );
381 + $args['paged'] = isset( $input['pages'] ) && $input['pages'] > 0 ? (int) $input['pages'] : 1;
382 +
383 + // Post type
384 + if ( ! empty( $input['postType'] ) ) {
385 + $args['post_type'] = $input['postType'];
386 + }
387 +
388 + // Order and orderby
389 + if ( ! empty( $input['order'] ) ) {
390 + $args['order'] = $input['order'];
391 + }
392 +
393 + if ( ! empty( $input['orderBy'] ) ) {
394 + $args['orderby'] = $input['orderBy'];
395 + }
396 +
397 + // Author
398 + if ( ! empty( $input['author'] ) ) {
399 + $args['author'] = $input['author'];
400 + }
401 +
402 + // Search
403 + if ( ! empty( $input['search'] ) ) {
404 + $args['s'] = $input['search'];
405 + }
406 +
407 + // Exclude posts
408 + if ( ! empty( $input['exclude'] ) && is_array( $input['exclude'] ) ) {
409 + $args['post__not_in'] = array_map( 'intval', $input['exclude'] );
410 + }
411 +
412 + // Sticky posts
413 + if ( ! empty( $input['sticky'] ) ) {
414 + if ( $input['sticky'] === 'include' ) {
415 + $args['ignore_sticky_posts'] = 0;
416 + } elseif ( $input['sticky'] === 'exclude' ) {
417 + $args['ignore_sticky_posts'] = 1;
418 + }
419 + }
420 +
421 + // Post parent (for hierarchical post types like pages)
422 + if ( ! empty( $input['parents'] ) && is_array( $input['parents'] ) ) {
423 + $args['post_parent__in'] = array_map( 'intval', $input['parents'] );
424 + }
425 +
426 + // Offset
427 + if ( isset( $input['offset'] ) ) {
428 + $args['offset'] = (int) $input['offset'];
429 + }
430 +
431 + // Format (post format taxonomy)
432 + if ( ! empty( $input['format'] ) ) {
433 + $args['tax_query'][] = [
434 + 'taxonomy' => 'post_format',
435 + 'field' => 'slug',
436 + 'terms' => [ 'post-format-' . sanitize_title( $input['format'] ) ],
437 + ];
438 + }
439 +
440 + // Category
441 + if ( ! empty( $input['category'] ) ) {
442 + $args['category_name'] = $input['category'];
443 + }
444 +
445 + // Tag
446 + if ( ! empty( $input['tag'] ) ) {
447 + $args['tag'] = $input['tag'];
448 + }
449 +
450 + // Generic taxonomy query
451 + if ( ! empty( $input['taxonomy'] ) && ! empty( $input['value'] ) ) {
452 + $args['tax_query'][] = [
453 + 'taxonomy' => $input['taxonomy'],
454 + 'field' => 'slug',
455 + 'terms' => is_array( $input['value'] ) ? $input['value'] : [ $input['value'] ],
456 + ];
457 + }
458 +
459 + return $args;
460 + }
461 +}