PluginProbe
Content Blocks Builder – Create blocks, repeater blocks with carousel, grid, popup layouts / 2.8.2
Content Blocks Builder – Create blocks, repeater blocks with carousel, grid, popup layouts v2.8.2
2.8.14 2.8.13 2.8.12 2.7.5 2.7.6 2.7.7 2.7.8 2.7.9 2.8.0 2.8.1 2.8.10 2.8.11 2.8.2 2.8.3 2.8.4 2.8.5 2.8.6 2.8.7 2.8.8 2.8.9 trunk 1.0.1 1.0.2 1.1.0 1.1.1 All 147 releases
content-blocks-builder / includes / custom-blocks.php

custom-blocks.php in Content Blocks Builder – Create blocks, repeater blocks with carousel, grid, popup layouts 2.8.2, at includes/custom-blocks.php

2,631 lines 80.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Register custom blocks
4 *
5 * @package BoldBlocks
6 * @author Phi Phan <mrphipv@gmail.com>
7 * @copyright Copyright (c) 2022, Phi Phan
8 */
9
10 namespace BoldBlocks;
11
12 // Exit if accessed directly.
13 defined( 'ABSPATH' ) || exit;
14
15 if ( ! class_exists( CustomBlocks::class ) ) :
16 /**
17 * Create/edit custom content blocks.
18 */
19 class CustomBlocks extends CoreComponent {
20 /**
21 * Post type
22 *
23 * @var string
24 */
25 protected $post_type = 'boldblocks_block';
26
27 /**
28 * Post type helper instance
29 *
30 * @var PostType
31 */
32 protected $post_type_helper;
33
34 /**
35 * Store all custom blocks
36 *
37 * @var array
38 */
39 protected $all_custom_blocks = [];
40
41 /**
42 * Store all names of custom blocks
43 *
44 * @var array
45 */
46 protected $custom_blocks = [];
47
48 /**
49 * Store all names of repeater blocks
50 *
51 * @var array
52 */
53 protected $repeater_blocks = [];
54
55 /**
56 * Max items that can query
57 *
58 * @var integer
59 */
60 protected $max_items = 200;
61
62 /**
63 * The handle for custom blocks scripts
64 *
65 * @var string
66 */
67 public $custom_blocks_handle = 'boldblocks-custom-blocks';
68
69 /**
70 * The handle for custom blocks frontend scripts
71 *
72 * @var string
73 */
74 public $custom_blocks_frontend_handle = 'boldblocks-custom-blocks-frontend';
75
76 /**
77 * The handle for custom blocks editor scripts
78 *
79 * @var string
80 */
81 public $custom_blocks_editor_handle = 'boldblocks-custom-blocks-editor';
82
83 /**
84 * The handle for carousel blocks frontend scripts
85 *
86 * @var string
87 */
88 public $carousel_blocks_frontend_handle = 'boldblocks-carousel-frontend';
89
90 /**
91 * Store preview style for html wrapper blocks
92 *
93 * @var string
94 */
95 private $html_editor_style = '';
96
97 /**
98 * Constructor
99 */
100 public function __construct( $the_plugin_instance ) {
101 parent::__construct( $the_plugin_instance );
102
103 $this->post_type_helper = PostType::get_instance();
104 }
105
106 /**
107 * Run main hooks
108 *
109 * @return void
110 */
111 public function run() {
112 // Register custom post type.
113 add_action( 'init', [ $this, 'register_custom_post_type' ], 5 );
114
115 // Custom admin columns.
116 add_filter( 'manage_edit-' . $this->post_type . '_columns', [ $this, 'add_block_custom_columns' ] );
117 add_action( 'manage_' . $this->post_type . '_posts_custom_column', [ $this, 'manage_block_columns' ], 10, 2 );
118 add_action( 'admin_head', [ $this, 'add_block_custom_columns_style' ] );
119
120 // Register scripts, make sure it is placed before load_custom_blocks.
121 add_action( 'init', [ $this, 'register_scripts' ] );
122
123 // Load custom blocks.
124 add_action( 'init', [ $this, 'load_custom_blocks' ] );
125
126 // Allow creating patterns from custom blocks.
127 add_filter( 'boldblocks_get_pattern_allowed_blocks', [ $this, 'allow_creating_patterns' ] );
128
129 // Add meta fields to meta revisioning.
130 add_filter( 'boldblocks_post_revision_meta_keys', [ $this, 'add_fields_to_revision_meta_keys' ] );
131
132 // Add theme name to body class.
133 add_filter( 'body_class', [ $this, 'add_body_class' ] );
134
135 // Add block version to custom blocks.
136 add_action( 'save_post_' . $this->post_type, [ $this, 'save_version_to_block' ], 10, 3 );
137
138 // Prevent users from deleting the core blocks.
139 add_filter( 'user_has_cap', [ $this, 'prevent_core_blocks_from_deletion' ], 10, 3 );
140
141 // Change the placeholder text.
142 add_filter( 'enter_title_here', [ $this, 'enter_title_here' ] );
143
144 // Register codemirror.
145 add_action( 'admin_enqueue_scripts', [ $this, 'register_code_editor_scripts' ] );
146
147 // Change the script src to use new version of esprima.
148 add_filter( 'script_loader_src', [ $this, 'adjust_esprima_script' ], 10, 2 );
149
150 // Handle save post.
151 add_action( 'save_post_' . $this->post_type, [ $this, 'save_post' ], 10, 2 );
152
153 // Clear the transient cache on upgraded.
154 add_action( 'cbb_version_upgraded', [ $this, 'clear_transient_cache' ] );
155
156 // Enqueue frontend scripts for non-cbb blocks.
157 add_filter( 'render_block', [ $this, 'enqueue_frontend_block_scripts' ], 10, 3 );
158
159 // Enqueue frontend scripts for carousel layout.
160 add_filter( 'render_block', [ $this, 'enqueue_frontend_carousel_scripts' ], 10, 3 );
161
162 // Register rest fields.
163 add_action( 'rest_api_init', [ $this, 'register_rest_fields' ] );
164
165 // Render inline script module.
166 add_filter( 'wp_inline_script_attributes', [ $this, 'add_inline_script_module' ] );
167
168 // Hide on the frontend.
169 add_filter( 'render_block', [ $this, 'hide_on_frontend' ], 10, 3 );
170
171 // Enqueue custom scripts & styles for custom blocks.
172 add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_custom_block_scripts' ] );
173
174 // Customize the Query Loop block.
175 add_action( 'init', [ $this, 'query_loop_extended_filters_and_sorting' ], PHP_INT_MAX );
176
177 // Enqueue styles for the html preview iframe.
178 add_filter( 'block_editor_settings_all', [ $this, 'enqueue_html_style_for_the_editor' ] );
179
180 // Strip all cbb meta fields.
181 add_filter( 'postmeta_form_keys', [ $this, 'strip_cbb_meta_keys' ] );
182
183 // Force to remove custom fields.
184 add_action( 'add_meta_boxes', [ $this, 'remove_meta_boxes' ] );
185 }
186
187 /**
188 * Register custom post type
189 *
190 * @return void
191 */
192 public function register_custom_post_type() {
193 // Register block type.
194 $this->post_type_helper->register_post_type(
195 $this->post_type,
196 [
197 'rest_base' => 'boldblocks-blocks',
198 'menu_icon' => 'dashicons-block-default',
199 'menu_position' => 80, // Right after Settings.
200 'capabilities' => [
201 'read_post' => 'manage_options',
202 'edit_post' => 'manage_options',
203 'delete_post' => 'manage_options',
204 'edit_posts' => 'manage_options',
205 'delete_posts' => 'manage_options',
206 ],
207 ],
208 [
209 'name' => _x( 'Custom Blocks', 'post type general name', 'content-blocks-builder' ),
210 'singular_name' => _x( 'Custom Block', 'post type singular name', 'content-blocks-builder' ),
211 'name_admin_bar' => _x( 'Block', 'add new on admin bar', 'content-blocks-builder' ),
212 'all_items' => __( 'All Blocks', 'content-blocks-builder' ),
213 'add_new' => __( 'Add New Block', 'content-blocks-builder' ),
214 'add_new_item' => __( 'Add New Block', 'content-blocks-builder' ),
215 ]
216 );
217
218 // Block keywords.
219 $this->post_type_helper->register_taxonomy(
220 'boldblocks_block_keywords',
221 $this->post_type,
222 [],
223 [
224 'name' => _x( 'Keywords', 'Taxonomy General Name', 'content-blocks-builder' ),
225 'singular_name' => _x( 'Keyword', 'Taxonomy Singular Name', 'content-blocks-builder' ),
226 ]
227 );
228
229 // Meta fields.
230 $this->register_meta(
231 'boldblocks_block_blocks',
232 [
233 'default' => '[]',
234 ]
235 );
236
237 $this->register_meta(
238 'boldblocks_block_dependent_blocks',
239 [
240 'show_in_rest' => array(
241 'schema' => array(
242 'items' => array(
243 'type' => 'string',
244 ),
245 ),
246 ),
247 'type' => 'array',
248 'default' => [],
249 ]
250 );
251
252 $this->register_meta( 'boldblocks_block_class' );
253
254 $this->register_meta( 'boldblocks_block_description' );
255
256 $this->register_meta( 'boldblocks_block_icon' );
257
258 $this->register_meta(
259 'boldblocks_block_supports',
260 [
261 'show_in_rest' => [
262 'schema' => [
263 'type' => 'object',
264 'additionalProperties' => array(
265 'type' => [ 'boolean' ], // can be ['boolean', 'object'...].
266 ),
267 ],
268 ],
269 'type' => 'object',
270 ]
271 );
272
273 $this->register_meta(
274 'boldblocks_template_lock',
275 [
276 'default' => 'false',
277 ]
278 );
279
280 $this->register_meta(
281 'boldblocks_enable_variation_picker',
282 [
283 'type' => 'boolean',
284 'default' => false,
285 ]
286 );
287
288 $this->register_meta(
289 'boldblocks_enable_repeater',
290 [
291 'type' => 'boolean',
292 'default' => false,
293 ]
294 );
295
296 $this->register_meta(
297 'boldblocks_parent_layout_type',
298 [
299 'default' => 'vstack',
300 ]
301 );
302
303 $this->register_meta( 'boldblocks_parent_block_icon' );
304
305 $this->register_meta(
306 'boldblocks_parent_block_supports',
307 [
308 'show_in_rest' => [
309 'schema' => [
310 'type' => 'object',
311 'additionalProperties' => array(
312 'type' => [ 'boolean' ], // can be ['boolean', 'object'...].
313 ),
314 ],
315 ],
316 'type' => 'object',
317 ]
318 );
319
320 $this->register_meta(
321 'boldblocks_parent_enable_variation_picker',
322 [
323 'type' => 'boolean',
324 'default' => false,
325 ]
326 );
327
328 $this->register_meta(
329 'boldblocks_disable_standalone',
330 [
331 'type' => 'boolean',
332 'default' => false,
333 ]
334 );
335
336 $this->register_meta(
337 'boldblocks_is_fixed_nested_item_count',
338 [
339 'type' => 'boolean',
340 'default' => false,
341 ]
342 );
343
344 $this->register_meta(
345 'boldblocks_nested_item_count',
346 [
347 'type' => 'integer',
348 'default' => 1,
349 ]
350 );
351
352 $this->register_meta( 'boldblocks_parent_block_name' );
353
354 $this->register_meta( 'boldblocks_parent_block_title' );
355
356 $this->register_meta( 'boldblocks_parent_block_description' );
357
358 $this->register_meta( 'boldblocks_parent_block_class' );
359
360 $this->register_meta( 'boldblocks_block_version' );
361
362 $this->register_meta(
363 'boldblocks_block_dependencies',
364 [
365 'type' => 'array',
366 'show_in_rest' => array(
367 'schema' => array(
368 'items' => array(
369 'type' => 'object',
370 'properties' => array(
371 'slug' => array(
372 'type' => 'string',
373 ),
374 'name' => array(
375 'type' => 'string',
376 ),
377 ),
378 ),
379 ),
380 ),
381 'default' => [],
382 ]
383 );
384
385 $this->register_meta(
386 'boldblocks_block_external_scripts',
387 [
388 'type' => 'array',
389 'show_in_rest' => array(
390 'schema' => array(
391 'items' => array(
392 'type' => 'object',
393 'properties' => array(
394 'handle' => array(
395 'type' => 'string',
396 'default' => '',
397 ),
398 'src' => array(
399 'type' => 'string',
400 'default' => '',
401 ),
402 'deps' => array(
403 'type' => 'string',
404 'default' => '',
405 ),
406 'version' => array(
407 'type' => 'string',
408 'default' => '',
409 ),
410 'in_footer' => array(
411 'type' => 'boolean',
412 'default' => false,
413 ),
414 'loading_strategy' => array(
415 'type' => 'string',
416 'default' => 'none',
417 ),
418 'in_backend' => array(
419 'type' => 'boolean',
420 'default' => false,
421 ),
422 ),
423 ),
424 ),
425 ),
426 'default' => [],
427 ]
428 );
429
430 $this->register_meta(
431 'boldblocks_block_custom_scripts',
432 [
433 'type' => 'array',
434 'show_in_rest' => array(
435 'schema' => array(
436 'items' => array(
437 'type' => 'object',
438 'properties' => array(
439 'value' => array(
440 'type' => 'string',
441 ),
442 'script_type' => array(
443 'type' => 'string',
444 'default' => '', // 'module', 'custom', 'default'.
445 ),
446 'handle' => array(
447 'type' => 'string',
448 ),
449 // is_module is deprecated. it will be removed in the future.
450 'is_module' => array(
451 'type' => 'boolean',
452 'default' => false,
453 ),
454 'deps' => array(
455 'type' => 'string',
456 'default' => '',
457 ),
458 ),
459 ),
460 ),
461 ),
462 'default' => [],
463 ]
464 );
465
466 $this->register_meta(
467 'boldblocks_block_external_styles',
468 [
469 'type' => 'array',
470 'show_in_rest' => array(
471 'schema' => array(
472 'items' => array(
473 'type' => 'object',
474 'properties' => array(
475 'handle' => array(
476 'type' => 'string',
477 'default' => '',
478 ),
479 'src' => array(
480 'type' => 'string',
481 'default' => '',
482 ),
483 'deps' => array(
484 'type' => 'string',
485 'default' => '',
486 ),
487 'version' => array(
488 'type' => 'string',
489 'default' => '',
490 ),
491 'media' => array(
492 'type' => 'string',
493 'default' => 'all',
494 ),
495 'in_backend' => array(
496 'type' => 'boolean',
497 'default' => false,
498 ),
499 ),
500 ),
501 ),
502 ),
503 'default' => [],
504 ]
505 );
506
507 $this->register_meta(
508 'boldblocks_block_custom_styles',
509 [
510 'type' => 'array',
511 'show_in_rest' => array(
512 'schema' => array(
513 'items' => array(
514 'type' => 'object',
515 'properties' => array(
516 'handle' => array(
517 'type' => 'string',
518 ),
519 'type' => array(
520 'type' => 'string',
521 ),
522 'value' => array(
523 'type' => 'string',
524 ),
525 ),
526 ),
527 ),
528 ),
529 'default' => [],
530 ]
531 );
532
533 $this->register_meta(
534 'boldblocks_block_attributes',
535 [
536 'type' => 'array',
537 'show_in_rest' => array(
538 'schema' => array(
539 'items' => array(
540 'type' => 'object',
541 'properties' => array(
542 'name' => array(
543 'type' => 'string',
544 ),
545 'type' => array(
546 'type' => 'string',
547 ),
548 ),
549 'additionalProperties' => array(
550 'type' => [ 'string', 'boolean', 'object', 'array' ],
551 ),
552 ),
553 ),
554 ),
555 'default' => [],
556 ]
557 );
558
559 $this->register_meta(
560 'boldblocks_parent_block_attributes',
561 [
562 'type' => 'array',
563 'show_in_rest' => array(
564 'schema' => array(
565 'items' => array(
566 'type' => 'object',
567 'properties' => array(
568 'name' => array(
569 'type' => 'string',
570 ),
571 'type' => array(
572 'type' => 'string',
573 ),
574 ),
575 'additionalProperties' => array(
576 'type' => [ 'string', 'boolean', 'object', 'array' ],
577 ),
578 ),
579 ),
580 ),
581 'default' => [],
582 ]
583 );
584
585 $this->register_meta(
586 'boldblocks_enable_custom_attributes',
587 [
588 'type' => 'boolean',
589 'default' => false,
590 ]
591 );
592
593 $this->register_meta(
594 'boldblocks_parent_enable_custom_attributes',
595 [
596 'type' => 'boolean',
597 'default' => false,
598 ]
599 );
600
601 $this->register_meta(
602 'boldblocks_hide_on_frontend',
603 [
604 'type' => 'boolean',
605 'default' => false,
606 ]
607 );
608
609 $this->register_meta(
610 'boldblocks_is_readonly',
611 [
612 'type' => 'boolean',
613 'default' => false,
614 ]
615 );
616
617 $this->register_meta(
618 'boldblocks_is_transformable',
619 [
620 'type' => 'boolean',
621 'default' => false,
622 ]
623 );
624
625 $this->register_meta(
626 'boldblocks_is_hidden',
627 [
628 'type' => 'boolean',
629 'default' => false,
630 ]
631 );
632
633 $this->register_meta( 'boldblocks_block_parent' );
634
635 $this->register_meta( 'boldblocks_block_ancestor' );
636
637 $this->register_meta( 'boldblocks_block_allowed_blocks' );
638 }
639
640 /**
641 * Register meta field
642 *
643 * @param string $meta_key
644 * @param array $args
645 * @return void
646 */
647 private function register_meta( $meta_key, $args = [] ) {
648 $this->post_type_helper->register_meta( $this->post_type, $meta_key, $args );
649 }
650
651 /**
652 * Don't show meta fields created CBB
653 *
654 * @param array $keys
655 * @return array
656 */
657 public function strip_cbb_meta_keys( $keys ) {
658 global $wpdb;
659
660 /**
661 * Filters the number of custom fields to retrieve for the drop-down
662 * in the Custom Fields meta box.
663 *
664 * @since 2.1.0
665 *
666 * @param int $limit Number of custom fields to retrieve. Default 30.
667 */
668 $limit = apply_filters( 'postmeta_form_limit', 30 );
669
670 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
671 $keys = $wpdb->get_col(
672 $wpdb->prepare(
673 "SELECT DISTINCT meta_key
674 FROM $wpdb->postmeta
675 WHERE meta_key NOT BETWEEN '_' AND '_z'
676 AND meta_key NOT LIKE %s
677 HAVING meta_key NOT LIKE %s
678 ORDER BY meta_key
679 LIMIT %d",
680 $wpdb->esc_like( 'boldblocks_' ) . '%',
681 $wpdb->esc_like( '_' ) . '%',
682 $limit
683 )
684 );
685
686 return $keys;
687 }
688
689 /**
690 * Remove custom meta boxes
691 *
692 * @param string $post_type
693 * @return void
694 */
695 public function remove_meta_boxes( $post_type ) {
696 if ( $post_type === $this->post_type ) {
697 remove_meta_box( 'postcustom', false, 'normal' );
698 }
699 }
700
701 /**
702 * Load all custom blocks
703 *
704 * @return void
705 */
706 public function load_custom_blocks() {
707 // Load all custom blocks.
708 $all_custom_blocks = $this->get_all_custom_blocks();
709
710 // Build an array of custom block name.
711 $this->custom_blocks = array_column( $all_custom_blocks, null, 'name' );
712
713 // Build an array of repeater blocks.
714 $this->repeater_blocks = array_filter(
715 array_map(
716 function ( $block_args ) {
717 return isset( $block_args['parent'] ) ? $block_args['parent'] : false;
718 },
719 $all_custom_blocks
720 )
721 );
722
723 // Make name as the key.
724 $this->repeater_blocks = array_column( $this->repeater_blocks, null, 'name' );
725
726 // Get current post.
727 $post = $this->get_current_post();
728 $block_name = '';
729 $repeater_block_name = '';
730
731 if ( $post && $this->post_type === $post->post_type ) {
732 $block_name = sprintf( 'boldblocks/%s', $this->sanitize_block_name( $post->post_name ) );
733
734 $enable_repeater = get_post_meta( $post->ID, 'boldblocks_enable_repeater', true );
735 if ( $enable_repeater ) {
736 $parent_name = get_post_meta( $post->ID, 'boldblocks_parent_block_name', true );
737 if ( ! $parent_name ) {
738 $parent_name = $post->post_name;
739 }
740 $repeater_block_name = sprintf( 'boldblocks/%s-repeater', $this->sanitize_block_name( $parent_name ) );
741 }
742 }
743
744 // Register all blocks.
745 foreach ( array_merge( $this->custom_blocks, $this->repeater_blocks ) as $name => $args ) {
746 // Build block attributes.
747 $block_atts = [
748 'api_version' => 3,
749 'supports' => [
750 'cbb' => true,
751 'layout' => true,
752 'migrateClass' => ! empty( $args['migrateClass'] ) ? ( isset( $args['id'] ) ? 'wp-block-boldblocks-custom' : 'wp-block-boldblocks-custom-parent' ) : '',
753 ],
754 'uses_context' => [ 'postId', 'postType', 'cbb/block-overrides' ],
755 ];
756
757 if ( $args['blockSupports']['background'] ?? false ) {
758 // New duotone in WP 6.3.
759 if ( ! ( $block_atts['supports']['filter'] ?? false ) ) {
760 $block_atts['supports']['filter'] = [];
761 }
762 $block_atts['supports']['filter']['duotone'] = true;
763
764 if ( ! ( $block_atts['selectors'] ?? false ) ) {
765 $block_atts['selectors'] = [];
766 }
767
768 if ( ! ( $block_atts['selectors']['filter'] ?? false ) ) {
769 $block_atts['selectors']['filter'] = [];
770 }
771
772 $block_atts['selectors']['filter']['duotone'] = '> .bb\\:block-background';
773 }
774
775 if ( ! isset( $args['parent'] ) ) {
776 if ( $args['blockParent'] ?? false ) {
777 $block_parent = $args['blockParent'];
778 if ( $block_name ) {
779 $index = array_search( $block_name, $block_parent, true );
780 if ( $index !== false ) {
781 unset( $block_parent[ $index ] );
782 $block_parent = array_values( $block_parent );
783 }
784 }
785
786 if ( $repeater_block_name ) {
787 $index = array_search( $repeater_block_name, $block_parent, true );
788 if ( $index !== false ) {
789 unset( $block_parent[ $index ] );
790 $block_parent = array_values( $block_parent );
791 }
792 }
793
794 if ( $block_parent ) {
795 $block_atts['parent'] = $block_parent;
796 }
797 }
798
799 if ( $args['blockAncestor'] ?? false ) {
800 $block_ancestor = $args['blockAncestor'];
801 if ( $block_name ) {
802 $index = array_search( $block_name, $block_ancestor, true );
803 if ( $index !== false ) {
804 unset( $block_ancestor[ $index ] );
805 $block_ancestor = array_values( $block_ancestor );
806 }
807 }
808
809 if ( $repeater_block_name ) {
810 $index = array_search( $repeater_block_name, $block_ancestor, true );
811 if ( $index !== false ) {
812 unset( $block_ancestor[ $index ] );
813 $block_ancestor = array_values( $block_ancestor );
814 }
815 }
816
817 if ( $block_ancestor ) {
818 $block_atts['ancestor'] = $block_ancestor;
819 }
820 }
821 }
822
823 // Styles and scripts.
824 $handles = [
825 'style_handles' => [ $this->custom_blocks_handle ],
826 'editor_style_handles' => [ $this->custom_blocks_editor_handle ],
827 'script_handles' => [],
828 'editor_script_handles' => [ $this->custom_blocks_handle ],
829 'view_script_handles' => [ $this->custom_blocks_frontend_handle ],
830 ];
831
832 $parent_layout_type = $args['layoutType'] ?? '';
833
834 if ( empty( $parent_layout_type ) && ! isset( $args['parent'] ) ) {
835 $block_atts['supports']['layoutType'] = 'standalone';
836 } else {
837 $block_layout_type = '';
838 if ( $parent_layout_type ) {
839 // Is parent block.
840 $block_layout_type = $parent_layout_type;
841 } else {
842 // Is child block.
843 $parent_layout = $args['parent']['layoutType'] ?? '';
844 $block_layout_type = $parent_layout . 'Item';
845 }
846 $block_atts['supports']['layoutType'] = $block_layout_type;
847 }
848
849 // Don't allow layout on repeater block except vstack layout.
850 if ( $parent_layout_type && 'vstack' !== $parent_layout_type ) {
851 $block_atts['supports']['layout'] = false;
852 }
853
854 // Don't allow layout on accordion item.
855 if ( 'accordion' === ( $args['parent']['layoutType'] ?? '' ) ) {
856 $block_atts['supports']['layout'] = false;
857 }
858
859 // Enqueue scripts for specific block types.
860 if ( 'carousel' === $parent_layout_type ) {
861 // Mark it as carousel item block.
862 $block_atts['supports']['parentLayoutType'] = 'carousel';
863
864 $handles['style_handles'][] = $this->carousel_blocks_frontend_handle;
865 $handles['script_handles'][] = $this->carousel_blocks_frontend_handle;
866 }
867
868 // Scripts & styles.
869 $is_backend = is_admin();
870 $name_handle = str_replace( '/', '-', $name );
871 $block_inline_handle = 'cbb-' . $name_handle;
872 $block_module_inline_handle = 'cbb-script-module-' . $name_handle;
873
874 if ( ! empty( $args['external_scripts'] ) ) {
875 $external_scripts = array_filter(
876 $args['external_scripts'],
877 function ( $script ) {
878 return $script['handle'] ?? false;
879 }
880 );
881
882 foreach ( $external_scripts as $script ) {
883 if ( $script['deps'] ?? false ) {
884 $deps = $this->refine_script_handle( $script['deps'], $is_backend );
885 $deps = \explode( ',', $deps );
886 } else {
887 $deps = [];
888 }
889 $version = $style['version'] ?? null;
890 if ( $version ) {
891 if ( 'WP' === $version ) {
892 $version = '';
893 } elseif ( 'CBB' === $version ) {
894 $version = BOLDBLOCKS_CBB_VERSION;
895 }
896 }
897
898 // Don't load external resources on admin side for some cases.
899 if ( ! $is_backend || ( $script['in_backend'] ?? false ) ) {
900 $loading_strategy_args = [
901 'in_footer' => isset( $script['in_footer'] ) ? $script['in_footer'] : true,
902 ];
903
904 $loading_strategy = $script['loading_strategy'] ?? '';
905 if ( in_array( $loading_strategy, [ 'defer', 'async' ], true ) ) {
906 $loading_strategy_args['strategy'] = $loading_strategy;
907 }
908
909 wp_register_script( $script['handle'], $script['src'] ?? '', $deps, $version, $loading_strategy_args );
910
911 // Add the handle to the block.
912 $handles['view_script_handles'][] = $script['handle'];
913
914 if ( $is_backend ) {
915 $handles['editor_script_handles'][] = $script['handle'];
916 }
917 }
918 }
919 }
920
921 if ( ! $is_backend && ! empty( $args['custom_scripts'] ) ) {
922 $custom_scripts = $args['custom_scripts'];
923
924 foreach ( $custom_scripts as $script ) {
925 if ( ! $script || ! is_array( $script ) || empty( $script['value'] ) ) {
926 continue;
927 }
928
929 $script_type = $script['script_type'] ?? '';
930 // Legacy support for script_type.
931 if ( ! $script_type ) {
932 if ( $script['is_module'] ?? false ) {
933 $script_type = 'module';
934 } elseif ( ! empty( $script['handle'] ) ) {
935 $script_type = 'custom';
936 } else {
937 $script_type = 'default';
938 }
939 }
940 $is_module = 'module' === $script_type;
941 $handle = $is_module ? $block_module_inline_handle : $block_inline_handle;
942
943 // Determine if a custom handle should be used.
944 if ( 'custom' === $script_type && ! empty( $script['handle'] ) ) {
945 $handle = $this->refine_script_handle( $script['handle'], $is_backend );
946 } else {
947 // Get dep handles.
948 $dep_handles = $script['deps'] ?? 'CBB_BLOCK_API';
949 if ( false === strpos( $dep_handles, 'CBB_BLOCK_API' ) ) {
950 if ( ! $dep_handles ) {
951 $dep_handles = 'CBB_BLOCK_API';
952 } else {
953 $dep_handles = 'CBB_BLOCK_API,' . $dep_handles;
954 }
955 }
956
957 // Register the default handle for module and inline scripts.
958 wp_register_script( $handle, '', explode( ',', $this->refine_script_handle( $dep_handles, $is_backend ) ), BOLDBLOCKS_CBB_VERSION, [ 'in_footer' => true ] );
959
960 // Add handle to view scripts and editor scripts if backend.
961 if ( ! in_array( $handle, $handles['view_script_handles'], true ) ) {
962 $handles['view_script_handles'][] = $handle;
963 }
964 if ( $is_backend ) {
965 if ( ! in_array( $handle, $handles['editor_script_handles'], true ) ) {
966 $handles['editor_script_handles'][] = $handle;
967 }
968 }
969 }
970
971 // Add inline script.
972 wp_add_inline_script( $handle, $script['value'] );
973 }
974 }
975
976 if ( ! empty( $args['external_styles'] ) ) {
977 $external_styles = array_filter(
978 $args['external_styles'],
979 function ( $style ) {
980 return $style['handle'] ?? false;
981 }
982 );
983
984 foreach ( $external_styles as $style ) {
985 if ( $style['deps'] ?? false ) {
986 $deps = $this->refine_style_handle( $style['deps'], $is_backend );
987 $deps = \explode( ',', $deps );
988 } else {
989 $deps = [];
990 }
991 $version = $style['version'] ?? null;
992 if ( $version ) {
993 if ( 'WP' === $version ) {
994 $version = '';
995 } elseif ( 'CBB' === $version ) {
996 $version = BOLDBLOCKS_CBB_VERSION;
997 }
998 }
999 // Don't load external resources on admin side for some cases.
1000 if ( ! $is_backend || ! ( $style['src'] ?? '' ) || ( $style['in_backend'] ?? false ) ) {
1001 wp_register_style( $style['handle'], $style['src'] ?? '', $deps, $version, $style['media'] ?? 'all' );
1002
1003 // Add the handle to the block.
1004 $handles['style_handles'][] = $style['handle'];
1005 }
1006 }
1007 }
1008
1009 if ( ! empty( $args['custom_styles'] ) ) {
1010 $custom_styles = $args['custom_styles'];
1011
1012 // Hanle preview style for wrapper blocks of core/HTML.
1013 $is_preview = ! empty( $args['dependentBlocks'] ) && is_array( $args['dependentBlocks'] ) && count( $args['dependentBlocks'] ) === 1 && 'core/html' === ( $args['dependentBlocks'][0] ?? '' );
1014
1015 foreach ( $custom_styles as $style ) {
1016 if ( $style && is_array( $style ) && $style['value'] ) {
1017 $snippet_type = $style['type'] ?? 'all';
1018 if ( ( 'view' === $snippet_type && $is_backend ) || ( 'editor' === $snippet_type && ! $is_backend ) ) {
1019 continue;
1020 }
1021
1022 $value = $style['value'];
1023 if ( $is_preview ) {
1024 $this->html_editor_style .= $value;
1025 }
1026
1027 if ( empty( $style['handle'] ) ) {
1028 $handle = $block_inline_handle;
1029 // phpcs:ignore WordPress.WP.EnqueuedResourceParameters.MissingVersion
1030 wp_register_style( $handle, '' );
1031
1032 // Add the default handle to the block.
1033 $handles['style_handles'][] = $handle;
1034 } else {
1035 $handle = $this->refine_style_handle( $style['handle'], $is_backend );
1036 }
1037 wp_add_inline_style( $handle, $value );
1038 }
1039 }
1040 }
1041
1042 // Hide on frontend.
1043 $block_atts['supports']['hideOnFrontend'] = $args['hideOnFrontend'] ?? false;
1044
1045 // Block keywords.
1046 if ( $args['keywords'] ?? false ) {
1047 $block_atts['keywords'] = $args['keywords'];
1048 }
1049
1050 // Synced block overrides.
1051 if ( $args['isSyncedBlock'] ?? false ) {
1052 $block_atts['supports']['block_id'] = $args['id'];
1053 $block_atts['supports']['block_content'] = $args['postContent'];
1054 $block_atts['render_callback'] = [ $this, 'render_readonly_block' ];
1055 }
1056
1057 // Enable interactivity.
1058 $block_atts['supports']['interactivity'] = [ 'clientNavigation' => true ];
1059
1060 register_block_type( $name, array_merge( $block_atts, $handles ) );
1061 }
1062 }
1063
1064 /**
1065 * Make sure the block name is valid
1066 *
1067 * @param string $name
1068 * @return string
1069 */
1070 private function sanitize_block_name( $name ) {
1071 return sanitize_key( str_replace( '_', '-', $name ) );
1072 }
1073
1074 /**
1075 * Handle script handle
1076 *
1077 * @param string $handle
1078 * @param boolean $is_backend
1079 * @return string
1080 */
1081 private function refine_script_handle( $handle, $is_backend = false ) {
1082 if ( $handle ) {
1083 $handle = str_replace( ' ', '', $handle );
1084 $handle = str_replace( 'CBB_BLOCK_API', $is_backend ? $this->custom_blocks_handle : $this->custom_blocks_frontend_handle, $handle );
1085 $handle = str_replace( 'CBB_CAROUSEL_API', $is_backend ? $this->custom_blocks_handle : $this->carousel_blocks_frontend_handle, $handle );
1086 }
1087
1088 return $handle;
1089 }
1090
1091 /**
1092 * Handle style handle
1093 *
1094 * @param string $handle
1095 * @param boolean $is_backend
1096 * @return string
1097 */
1098 private function refine_style_handle( $handle, $is_backend = false ) {
1099 if ( $handle ) {
1100 $handle = str_replace( 'CBB_BLOCK_STYLE', $this->custom_blocks_handle, $handle );
1101 $handle = str_replace( 'CBB_CAROUSEL_STYLE', $is_backend ? $this->custom_blocks_handle : $this->carousel_blocks_frontend_handle, $handle );
1102 }
1103
1104 return $handle;
1105 }
1106
1107 /**
1108 * Register code editor scripts
1109 *
1110 * @return void
1111 */
1112 public function register_code_editor_scripts() {
1113 $screen = get_current_screen();
1114 if ( $screen && $screen->is_block_editor ) {
1115 wp_enqueue_code_editor( array( 'type' => 'text/css' ) );
1116 wp_enqueue_code_editor( array( 'type' => 'javascript' ) );
1117 }
1118 }
1119
1120 /**
1121 * Adjust the src of the esprima script.
1122 *
1123 * @param string $src The source URL of the script.
1124 * @param string $handle The handle of the script.
1125 * @return string The adjusted source URL of the script.
1126 */
1127 public function adjust_esprima_script( $src, $handle ) {
1128 if ( 'esprima' === $handle ) {
1129 $src = 'https://cdn.jsdelivr.net/npm/esprima-next@5.8.4/dist/esprima.js';
1130 }
1131
1132 return $src;
1133 }
1134
1135 /**
1136 * Register scripts
1137 *
1138 * @return void
1139 */
1140 public function register_scripts() {
1141 // Custom blocks asset file.
1142 $custom_blocks_asset = $this->the_plugin_instance->include_file( "build/custom-blocks{$this->the_plugin_instance->get_script_suffix()}.asset.php" );
1143
1144 // Scripts.
1145 wp_register_script(
1146 $this->custom_blocks_handle,
1147 $this->the_plugin_instance->get_file_uri( "build/custom-blocks{$this->the_plugin_instance->get_script_suffix()}.js" ),
1148 array_merge( $custom_blocks_asset['dependencies'] ?? [], array( 'code-editor', 'csslint', 'jshint' ) ),
1149 $this->the_plugin_instance->get_script_version( $custom_blocks_asset ),
1150 [ 'in_footer' => true ]
1151 );
1152
1153 // Add translation.
1154 wp_set_script_translations( $this->custom_blocks_handle, 'content-blocks-builder' );
1155
1156 // Get all blocks.
1157 $all_custom_blocks = $this->get_all_custom_blocks();
1158 $current_post = $this->get_current_post();
1159 if ( $current_post && $this->post_type === $current_post->post_type ) {
1160 // Hide the current block.
1161 $all_custom_blocks = array_map(
1162 function ( $block ) use ( $current_post ) {
1163 if ( $block['id'] === $current_post->ID ) {
1164 $block['isHidden'] = true;
1165 }
1166 return $block;
1167 },
1168 $all_custom_blocks
1169 );
1170 }
1171
1172 // Define data.
1173 $blocks_scripts = [
1174 'blocks' => $all_custom_blocks,
1175 'variations' => $this->the_plugin_instance->get_component( Variations::class )->get_all_variations(),
1176 'variationNonce' => wp_create_nonce( 'cbb_variation_nonce' ),
1177 ];
1178
1179 // Get block name for current variation.
1180 $block_name = $this->the_plugin_instance->get_component( Variations::class )->get_block_name();
1181 if ( $block_name ) {
1182 $blocks_scripts['variationBlockName'] = $block_name;
1183 }
1184
1185 // Load all custom blocks for registration.
1186 wp_add_inline_script( $this->custom_blocks_handle, 'var CBBBlocks=' . wp_json_encode( $blocks_scripts ), 'before' );
1187
1188 // Frontend styles.
1189 wp_register_style(
1190 $this->custom_blocks_handle,
1191 $this->the_plugin_instance->get_file_uri( "build/custom-blocks{$this->the_plugin_instance->get_script_suffix()}.css" ),
1192 [],
1193 $this->the_plugin_instance->get_script_version( $custom_blocks_asset )
1194 );
1195
1196 // Editor styles.
1197 wp_register_style(
1198 $this->custom_blocks_editor_handle,
1199 $this->the_plugin_instance->get_file_uri( 'build/custom-blocks-editor.css' ),
1200 [],
1201 $this->the_plugin_instance->get_script_version( $custom_blocks_asset )
1202 );
1203
1204 // Custom blocks asset file.
1205 $custom_blocks_frontend_asset = $this->the_plugin_instance->include_file( "build/custom-blocks-frontend{$this->the_plugin_instance->get_script_suffix()}.asset.php" );
1206
1207 // Frontend script.
1208 wp_register_script(
1209 $this->custom_blocks_frontend_handle,
1210 $this->the_plugin_instance->get_file_uri( "build/custom-blocks-frontend{$this->the_plugin_instance->get_script_suffix()}.js" ),
1211 $custom_blocks_frontend_asset['dependencies'] ?? [],
1212 $this->the_plugin_instance->get_script_version( $custom_blocks_frontend_asset ),
1213 [ 'in_footer' => true ]
1214 );
1215
1216 // Add translation.
1217 wp_set_script_translations( $this->custom_blocks_frontend_handle, 'content-blocks-builder' );
1218
1219 // Carousel asset file.
1220 $caousel_frontend_asset = $this->the_plugin_instance->include_file( 'build/carousel-frontend.asset.php' );
1221
1222 // Carousel styles.
1223 wp_register_style(
1224 $this->carousel_blocks_frontend_handle,
1225 $this->the_plugin_instance->get_file_uri( 'build/carousel-frontend.css' ),
1226 [],
1227 $this->the_plugin_instance->get_script_version( $caousel_frontend_asset )
1228 );
1229
1230 // Carousel frontend script.
1231 wp_register_script(
1232 $this->carousel_blocks_frontend_handle,
1233 $this->the_plugin_instance->get_file_uri( 'build/carousel-frontend.js' ),
1234 $caousel_frontend_asset['dependencies'] ?? [],
1235 $this->the_plugin_instance->get_script_version( $caousel_frontend_asset ),
1236 [ 'in_footer' => true ]
1237 );
1238
1239 // Add translation.
1240 wp_set_script_translations( $this->carousel_blocks_frontend_handle, 'content-blocks-builder' );
1241 }
1242
1243 /**
1244 * Get all custom blocks.
1245 *
1246 * @return array
1247 */
1248 public function get_all_custom_blocks() {
1249 if ( ! $this->all_custom_blocks ) {
1250 $this->all_custom_blocks = $this->get_posts( $this->the_plugin_instance->is_debug_mode() );
1251 }
1252
1253 return $this->all_custom_blocks;
1254 }
1255
1256 /**
1257 * Query posts and cache the results.
1258 *
1259 * @param bool $force_refresh Optional. Whether to force the cache to be refreshed. Default false.
1260 * @return array Array of WP_Post objects.
1261 */
1262 public function get_posts( $force_refresh = false ) {
1263 if ( $force_refresh ) {
1264 return $this->query_posts();
1265 }
1266
1267 $cache_key = 'bb_block_posts';
1268 $posts = get_transient( $cache_key );
1269
1270 // If nothing is found, build the object.
1271 if ( false === $posts ) {
1272 // Query posts.
1273 $posts = $this->query_posts();
1274
1275 if ( ! is_wp_error( $posts ) && count( $posts ) > 0 ) {
1276 set_transient( $cache_key, $posts, HOUR_IN_SECONDS );
1277 }
1278 }
1279
1280 return $posts;
1281 }
1282
1283 /**
1284 * Query and parse blocks
1285 *
1286 * @return array
1287 */
1288 public function query_posts() {
1289 $post_args = apply_filters(
1290 'cbb_block_query_args',
1291 [
1292 'post_type' => $this->post_type,
1293 'posts_per_page' => $this->get_max_items(),
1294 'orderby' => [
1295 'menu_order' => 'DESC',
1296 'date' => 'DESC',
1297 ],
1298 ]
1299 );
1300
1301 $raw_posts = get_posts( $post_args );
1302
1303 $block_posts = array_map(
1304 function ( $item ) {
1305 // Template lock.
1306 $template_lock = get_post_meta( $item->ID, 'boldblocks_template_lock', true );
1307
1308 // Convert boolean string to boolean.
1309 $template_lock = in_array( $template_lock, [ 'false', 'inherit' ], true ) ? false : $template_lock;
1310
1311 // Scripts & styles.
1312 $block_external_scripts = get_post_meta( $item->ID, 'boldblocks_block_external_scripts', true );
1313 $block_custom_scripts = get_post_meta( $item->ID, 'boldblocks_block_custom_scripts', true );
1314 $block_external_styles = get_post_meta( $item->ID, 'boldblocks_block_external_styles', true );
1315 $block_custom_styles = get_post_meta( $item->ID, 'boldblocks_block_custom_styles', true );
1316
1317 // Hide on frontend.
1318 $hide_on_frontend = (bool) get_post_meta( $item->ID, 'boldblocks_hide_on_frontend', true );
1319
1320 // Is a readonly block.
1321 $is_readonly = (bool) get_post_meta( $item->ID, 'boldblocks_is_readonly', true );
1322
1323 // Is block transformable.
1324 $is_transformable = (bool) get_post_meta( $item->ID, 'boldblocks_is_transformable', true );
1325
1326 // Is block hidden.
1327 $is_hidden = (bool) get_post_meta( $item->ID, 'boldblocks_is_hidden', true );
1328
1329 // Parent & ancestor.
1330 $block_parent = get_post_meta( $item->ID, 'boldblocks_block_parent', true );
1331 $block_parent = $block_parent ? array_filter( explode( ',', $block_parent ) ) : [];
1332 $block_ancestor = get_post_meta( $item->ID, 'boldblocks_block_ancestor', true );
1333 $block_ancestor = $block_ancestor ? array_filter( explode( ',', $block_ancestor ) ) : [];
1334 $allowed_blocks = get_post_meta( $item->ID, 'boldblocks_block_allowed_blocks', true );
1335 $allowed_blocks = $allowed_blocks ? array_filter( explode( ',', $allowed_blocks ) ) : [];
1336
1337 $keywords = get_the_terms( $item->ID, 'boldblocks_block_keywords' );
1338 if ( $keywords && ! is_wp_error( $keywords ) ) {
1339 $keywords = wp_list_pluck( $keywords, 'name' );
1340 } else {
1341 $keywords = false;
1342 }
1343
1344 $block_name = sprintf( 'boldblocks/%s', $this->sanitize_block_name( $item->post_name ) );
1345 $block_version = get_post_meta( $item->ID, 'boldblocks_block_version', true );
1346 $migrateClass = version_compare( $block_version, '2.7.14', '<=' );
1347
1348 $block_args = [
1349 'id' => $item->ID,
1350 'name' => $block_name,
1351 'title' => wp_strip_all_tags( $item->post_title ),
1352 'postContent' => $item->post_content,
1353 'blocks' => get_post_meta( $item->ID, 'boldblocks_block_blocks', true ),
1354 'description' => get_post_meta( $item->ID, 'boldblocks_block_description', true ),
1355 'templateLock' => $template_lock,
1356 'className' => get_post_meta( $item->ID, 'boldblocks_block_class', true ),
1357 'blockIcon' => get_post_meta( $item->ID, 'boldblocks_block_icon', true ),
1358 'blockSupports' => get_post_meta( $item->ID, 'boldblocks_block_supports', true ),
1359 'blockAttributes' => [
1360 'attributes' => get_post_meta( $item->ID, 'boldblocks_block_attributes', true ),
1361 'enableCustomAttributes' => (bool) get_post_meta( $item->ID, 'boldblocks_enable_custom_attributes', true ),
1362 ],
1363 'enableVariationPicker' => (bool) get_post_meta( $item->ID, 'boldblocks_enable_variation_picker', true ),
1364 'blockVersion' => $block_version,
1365 'migrateClass' => $migrateClass,
1366 'dependentBlocks' => get_post_meta( $item->ID, 'boldblocks_block_dependent_blocks', true ),
1367 'hideOnFrontend' => $hide_on_frontend,
1368 'isTransformable' => $is_transformable,
1369 'isHidden' => $is_hidden,
1370 'isSyncedBlock' => $is_readonly, // Only for standalone blocks.
1371 'allowedBlocks' => $allowed_blocks,
1372 ];
1373
1374 // Get parent block parameters.
1375 $enable_repeater = get_post_meta( $item->ID, 'boldblocks_enable_repeater', true );
1376 if ( $enable_repeater ) {
1377 $parent_name = get_post_meta( $item->ID, 'boldblocks_parent_block_name', true );
1378 if ( ! $parent_name ) {
1379 $parent_name = $item->post_name;
1380 }
1381
1382 $parent_block_name = sprintf( 'boldblocks/%s-repeater', $this->sanitize_block_name( $parent_name ) );
1383
1384 $parent_layout_type = get_post_meta( $item->ID, 'boldblocks_parent_layout_type', true );
1385
1386 $parent_title = get_post_meta( $item->ID, 'boldblocks_parent_block_title', true );
1387 if ( ! $parent_title ) {
1388 // translators: Repeater block title.
1389 $parent_title = sprintf( __( 'Repeater: %s', 'content-blocks-builder' ), $item->post_title );
1390 }
1391
1392 $parent_args = [
1393 'name' => $parent_block_name,
1394 'title' => wp_strip_all_tags( $parent_title ),
1395 'description' => get_post_meta( $item->ID, 'boldblocks_parent_block_description', true ),
1396 'className' => get_post_meta( $item->ID, 'boldblocks_parent_block_class', true ) ?? '',
1397 'blockIcon' => get_post_meta( $item->ID, 'boldblocks_parent_block_icon', true ),
1398 'blockSupports' => get_post_meta( $item->ID, 'boldblocks_parent_block_supports', true ),
1399 'blockAttributes' => [
1400 'attributes' => get_post_meta( $item->ID, 'boldblocks_parent_block_attributes', true ),
1401 'enableCustomAttributes' => (bool) get_post_meta( $item->ID, 'boldblocks_parent_enable_custom_attributes', true ),
1402 ],
1403 'enableVariationPicker' => (bool) get_post_meta( $item->ID, 'boldblocks_parent_enable_variation_picker', true ),
1404 'external_scripts' => $block_external_scripts,
1405 'custom_scripts' => $this->refine_custom_scripts( $block_custom_scripts, 'JS', $parent_block_name, $block_name ),
1406 'external_styles' => $block_external_styles,
1407 'custom_styles' => $this->refine_custom_scripts( $block_custom_styles, 'CSS', $parent_block_name, $block_name ),
1408 'hideOnFrontend' => $hide_on_frontend,
1409 'blockParent' => $block_parent,
1410 'blockAncestor' => $block_ancestor,
1411 'migrateClass' => $migrateClass,
1412
1413 ];
1414
1415 if ( $keywords ) {
1416 $parent_args['keywords'] = $keywords;
1417 }
1418
1419 $is_fixed_nested_item_count = get_post_meta( $item->ID, 'boldblocks_is_fixed_nested_item_count', true );
1420 if ( $is_fixed_nested_item_count ) {
1421 $nested_item_count = get_post_meta( $item->ID, 'boldblocks_nested_item_count', true );
1422 $nested_item_count = $nested_item_count ? absint( $nested_item_count ) : 1;
1423 $parent_args['nestedItemCount'] = $nested_item_count ? $nested_item_count : 1;
1424 }
1425
1426 $parent_args['layoutType'] = $parent_layout_type;
1427 $block_args['parent'] = $parent_args;
1428 $block_args['parentName'] = $parent_block_name;
1429
1430 $block_args['standalone'] = ! get_post_meta( $item->ID, 'boldblocks_disable_standalone', true );
1431 } else {
1432 if ( $keywords ) {
1433 $block_args['keywords'] = $keywords;
1434 }
1435
1436 $block_args = array_merge(
1437 $block_args,
1438 [
1439 'external_scripts' => $block_external_scripts,
1440 'custom_scripts' => $this->refine_custom_scripts( $block_custom_scripts, 'JS', $block_name ),
1441 'external_styles' => $block_external_styles,
1442 'custom_styles' => $this->refine_custom_scripts( $block_custom_styles, 'CSS', $block_name ),
1443 'blockParent' => $block_parent,
1444 'blockAncestor' => $block_ancestor,
1445 ]
1446 );
1447 }
1448
1449 return $block_args;
1450 },
1451 $raw_posts
1452 );
1453
1454 return $block_posts;
1455 }
1456
1457 /**
1458 * Refine custom scripts & styles
1459 *
1460 * @param array $custom_scripts
1461 * @param string $type
1462 * @param string $block_name
1463 * @return array
1464 */
1465 private function refine_custom_scripts( $custom_scripts, $type, $block_name, $child_block_name = '' ) {
1466 if ( $custom_scripts && is_array( $custom_scripts ) ) {
1467 $custom_style_handler = $this->the_plugin_instance->get_component( CustomStyle::class );
1468 $block_class = '.wp-block-' . str_replace( '/', '-', $block_name );
1469 $child_block_class = $child_block_name ? '.wp-block-' . str_replace( '/', '-', $child_block_name ) : '';
1470
1471 $custom_scripts = array_filter(
1472 $custom_scripts,
1473 function ( $script ) {
1474 return ! empty( $script['value'] );
1475 }
1476 );
1477
1478 $custom_scripts = array_map(
1479 function ( $script ) use ( $custom_style_handler, $block_class, $child_block_class, $type ) {
1480 $tokens = [];
1481 if ( 'CSS' === $type ) {
1482 if ( $child_block_class ) {
1483 $tokens['childblockselector'] = $child_block_class;
1484 }
1485 $tokens['selector'] = $block_class;
1486 } else {
1487 if ( $child_block_class ) {
1488 $tokens['CHILDBLOCKSELECTOR'] = $child_block_class;
1489 }
1490 $tokens['BLOCKSELECTOR'] = $block_class;
1491 }
1492
1493 $script['value'] = $custom_style_handler->refine_custom_value( $script['value'], $tokens, $type );
1494
1495 return $script;
1496 },
1497 $custom_scripts
1498 );
1499 }
1500
1501 return $custom_scripts;
1502 }
1503
1504 /**
1505 * Enqueue styles for html blocks in the editor
1506 *
1507 * @param array $editor_settings
1508 * @return void
1509 */
1510 public function enqueue_html_style_for_the_editor( $editor_settings ) {
1511 if ( $this->html_editor_style ) {
1512 $editor_settings['styles'][] = [ 'css' => $this->html_editor_style ];
1513 }
1514
1515 return $editor_settings;
1516 }
1517
1518 /**
1519 * Handle save block post
1520 *
1521 * @param int $post_id
1522 * @param WP_Post $post
1523 * @return void
1524 */
1525 public function save_post( $post_id, $post ) {
1526 // Ignore auto-draft status.
1527 if ( 'auto-draft' === $post->post_status ) {
1528 return;
1529 }
1530
1531 // Ignore autosave.
1532 if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
1533 return;
1534 }
1535
1536 $this->clear_transient_cache();
1537 }
1538
1539 /**
1540 * Clear transient cache
1541 *
1542 * @return void
1543 */
1544 public function clear_transient_cache() {
1545 delete_transient( 'bb_block_posts' );
1546 }
1547
1548 /**
1549 * Add custom blocks to the list of blocks that can create patterns from.
1550 *
1551 * @param array $allowed_blocks
1552 * @return array
1553 */
1554 public function allow_creating_patterns( $allowed_blocks ) {
1555 return array_merge( $allowed_blocks, array_keys( $this->custom_blocks ), array_keys( $this->repeater_blocks ) );
1556 }
1557
1558 /**
1559 * Add custom columns to custom post type
1560 *
1561 * @param array $columns The array of columns.
1562 * @return array
1563 */
1564 public function add_block_custom_columns( $columns ) {
1565 $custom_columns = array(
1566 'block_icon' => esc_html__( 'Block icon', 'content-blocks-builder' ),
1567 'block_name' => esc_html__( 'Block name', 'content-blocks-builder' ),
1568 'parent_block_icon' => esc_html__( 'Parent icon', 'content-blocks-builder' ),
1569 'parent_block_name' => esc_html__( 'Parent block name', 'content-blocks-builder' ),
1570 );
1571
1572 if ( apply_filters( 'cbb_display_block_version', $this->the_plugin_instance->is_debug_mode() ) ) {
1573 $custom_columns['block_version'] = esc_html__( 'Block version', 'content-blocks-builder' );
1574 }
1575
1576 // Add after title column.
1577 $return = array_slice( $columns, 0, 2, true ) + $custom_columns + array_slice( $columns, 2, count( $columns ) - 2, true );
1578
1579 return $return;
1580 }
1581
1582 /**
1583 * Manage custom columns.
1584 *
1585 * @param string $column the column name.
1586 * @param int $post_id the post id.
1587 */
1588 public function manage_block_columns( $column, $post_id ) {
1589 switch ( $column ) {
1590 case 'block_icon':
1591 $block_icon = get_post_meta( $post_id, 'boldblocks_block_icon', true );
1592 if ( empty( $block_icon ) ) {
1593 $block_icon = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" class="bb-icon bb-icon--block-default"><path d="M19 8h-1V6h-5v2h-2V6H6v2H5c-1.1 0-2 .9-2 2v8c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2v-8c0-1.1-.9-2-2-2zm.5 10c0 .3-.2.5-.5.5H5c-.3 0-.5-.2-.5-.5v-8c0-.3.2-.5.5-.5h14c.3 0 .5.2.5.5v8z"/></svg>';
1594 }
1595 echo $block_icon; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
1596 break;
1597
1598 case 'block_name':
1599 echo esc_html( 'boldblocks/' . $this->sanitize_block_name( get_post_field( 'post_name', $post_id ) ) );
1600 break;
1601
1602 case 'parent_block_icon':
1603 if ( get_post_meta( $post_id, 'boldblocks_enable_repeater', true ) ) {
1604 $block_icon = get_post_meta( $post_id, 'boldblocks_parent_block_icon', true );
1605 if ( empty( $block_icon ) ) {
1606 $block_icon = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" class="bb-icon bb-icon--block-default"><path d="M19 8h-1V6h-5v2h-2V6H6v2H5c-1.1 0-2 .9-2 2v8c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2v-8c0-1.1-.9-2-2-2zm.5 10c0 .3-.2.5-.5.5H5c-.3 0-.5-.2-.5-.5v-8c0-.3.2-.5.5-.5h14c.3 0 .5.2.5.5v8z"/></svg>';
1607 }
1608 echo $block_icon; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
1609 } else {
1610 esc_html_e( 'NA', 'content-blocks-builder' );
1611 }
1612 break;
1613
1614 case 'parent_block_name':
1615 $enable_repeater = get_post_meta( $post_id, 'boldblocks_enable_repeater', true );
1616 if ( $enable_repeater ) {
1617 echo esc_html( 'boldblocks/' . $this->sanitize_block_name( get_post_field( 'post_name', $post_id ) ) . '-repeater' );
1618 } else {
1619 esc_html_e( 'NA', 'content-blocks-builder' );
1620 }
1621 break;
1622
1623 case 'block_version':
1624 $block_version = get_post_meta( $post_id, 'boldblocks_block_version', true );
1625 if ( $block_version ) {
1626 echo esc_html( $block_version );
1627 } else {
1628 echo esc_html( '1.x' );
1629 }
1630 break;
1631
1632 // Just break out of the switch statement for everything else.
1633 default:
1634 break;
1635 }
1636 }
1637
1638 /**
1639 * Add style to custom columns
1640 *
1641 * @return void
1642 */
1643 public function add_block_custom_columns_style() {
1644 echo '<style>.column-block_icon, .column-parent_block_icon {width: 70px;}.column-block_icon svg,.column-parent_block_icon svg {max-width: 32px;max-height: 32px;}</style>';
1645 }
1646
1647 /**
1648 * Add custom block's meta fields to meta revisioning.
1649 *
1650 * @param array $fields
1651 * @return array
1652 */
1653 public function add_fields_to_revision_meta_keys( $fields ) {
1654 return array_merge(
1655 $fields,
1656 [
1657 'boldblocks_block_blocks',
1658 'boldblocks_block_class',
1659 'boldblocks_block_description',
1660 'boldblocks_block_icon',
1661 'boldblocks_block_supports',
1662 'boldblocks_template_lock',
1663 'boldblocks_enable_variation_picker',
1664 'boldblocks_enable_repeater',
1665 'boldblocks_parent_layout_type',
1666 'boldblocks_parent_block_icon',
1667 'boldblocks_parent_block_supports',
1668 'boldblocks_parent_enable_variation_picker',
1669 'boldblocks_disable_standalone',
1670 'boldblocks_is_fixed_nested_item_count',
1671 'boldblocks_nested_item_count',
1672 'boldblocks_parent_block_name',
1673 'boldblocks_parent_block_title',
1674 'boldblocks_parent_block_description',
1675 'boldblocks_parent_block_class',
1676 'boldblocks_block_custom_scripts',
1677 'boldblocks_block_custom_styles',
1678 'boldblocks_block_attributes',
1679 'boldblocks_parent_block_attributes',
1680 ]
1681 );
1682 }
1683
1684 /**
1685 * Add theme name to body class
1686 *
1687 * @param array $classes
1688 * @return array
1689 */
1690 public function add_body_class( $classes ) {
1691 // Add a frontend CSS class.
1692 $classes[] = 'cbb-frontend';
1693
1694 // Add theme slug.
1695 $classes[] = get_stylesheet();
1696
1697 return $classes;
1698 }
1699
1700 /**
1701 * Save the plugin version to custom blocks
1702 *
1703 * @param int $post_id
1704 * @param WP_Post $post
1705 * @param boolean $update
1706 * @return void
1707 */
1708 public function save_version_to_block( $post_id, $post, $update ) {
1709 // Bail if it is a update action.
1710 if ( $update ) {
1711 return;
1712 }
1713
1714 // Save the plugin version to blocks.
1715 update_post_meta( $post_id, 'boldblocks_block_version', $this->the_plugin_instance->get_plugin_version() );
1716 }
1717
1718 /**
1719 * Prevent users from deleting core blocks.
1720 *
1721 * @param array $allcaps
1722 * @param array $caps
1723 * @param array $args
1724 * @return array
1725 */
1726 public function prevent_core_blocks_from_deletion( $allcaps, $caps, $args ) {
1727 // Bail out if we're not asking about deleting a post.
1728 if ( 'delete_post' !== $args[0] ) {
1729 return $allcaps;
1730 }
1731
1732 // Bail out for users who can't delete posts.
1733 if ( empty( $allcaps['delete_posts'] ) ) {
1734 return $allcaps;
1735 }
1736
1737 // Load the post data.
1738 $post = get_post( $args[2] );
1739
1740 // Bail out if the post type is not boldblocks_block.
1741 if ( $this->post_type !== $post->post_type ) {
1742 return $allcaps;
1743 }
1744
1745 // Don't allow deleting core blocks.
1746 if ( \in_array( $post->post_name, [ 'group', 'carousel-item', 'grid-item', 'stack-item', 'accordion-item' ], true ) ) {
1747 $allcaps[ $caps[0] ] = false;
1748 }
1749
1750 return $allcaps;
1751 }
1752
1753 /**
1754 * Placeholder text. Default 'Add title'.
1755 *
1756 * @param string $text
1757 * @return string
1758 */
1759 public function enter_title_here( $text ) {
1760 $post_type = get_post_type();
1761 if ( $this->post_type === $post_type ) {
1762 $text = __( 'Add block title', 'content-blocks-builder' );
1763 }
1764
1765 return $text;
1766 }
1767
1768 /**
1769 * Enqueue frontend scripts for non-cbb blocks
1770 *
1771 * @param string $block_content
1772 * @param array $block
1773 * @param WP_Block $block_instance
1774 * @return string
1775 */
1776 public function enqueue_frontend_block_scripts( $block_content, $block, $block_instance ) {
1777 // Ignore admin side.
1778 if ( is_admin() ) {
1779 return $block_content;
1780 }
1781
1782 if ( isset( $GLOBALS['boldblocks_frontend_loaded'] ) ) {
1783 return $block_content;
1784 }
1785
1786 if ( $block_instance->block_type->supports['cbb'] ?? false ) {
1787 $GLOBALS['boldblocks_frontend_loaded'] = true;
1788 } elseif ( $this->is_enqueue_frontend_script_for_non_cbb_blocks( $block, $block_instance ) ) {
1789 wp_enqueue_script( $this->custom_blocks_frontend_handle );
1790 wp_enqueue_style( $this->custom_blocks_handle );
1791
1792 $GLOBALS['boldblocks_frontend_loaded'] = true;
1793 }
1794
1795 return $block_content;
1796 }
1797
1798 /**
1799 * Whether to load the frontend scripts for non-cbb blocks.
1800 *
1801 * @param array $block
1802 * @param WP_Block $block_instance
1803 * @return boolean
1804 */
1805 private function is_enqueue_frontend_script_for_non_cbb_blocks( $block, $block_instance ) {
1806 $block_names = $this->the_plugin_instance->get_component( CustomStyle::class )->non_cbb_blocks;
1807 if ( ! in_array( $block['blockName'] ?? '', $block_names, true ) ) {
1808 return false;
1809 }
1810
1811 // Has sticky enabling.
1812 if ( $block['attrs']['boldblocks']['sticky']['enable'] ?? false ) {
1813 return true;
1814 }
1815
1816 return false;
1817 }
1818
1819 /**
1820 * Enqueue frontend scripts for carousel layout in the query loop block
1821 *
1822 * @param string $block_content
1823 * @param array $block
1824 * @param WP_Block $block_instance
1825 * @return string
1826 */
1827 public function enqueue_frontend_carousel_scripts( $block_content, $block, $block_instance ) {
1828 // Ignore admin side.
1829 if ( is_admin() ) {
1830 return $block_content;
1831 }
1832
1833 if ( isset( $GLOBALS['boldblocks_carousel_loaded'] ) ) {
1834 return $block_content;
1835 }
1836
1837 // Make sure we have the blockName.
1838 if ( empty( $block['blockName'] ) ) {
1839 return $block_content;
1840 }
1841
1842 if ( 'core/query' === $block['blockName'] ) {
1843 // If this is a core/query block, enqueue the carousel script.
1844 if ( $this->is_enqueue_carousel_script_for_query_loop( $block, $block_instance ) ) {
1845 wp_enqueue_script( $this->carousel_blocks_frontend_handle );
1846 wp_enqueue_style( $this->carousel_blocks_frontend_handle );
1847
1848 $GLOBALS['boldblocks_carousel_loaded'] = true;
1849 }
1850 } elseif ( strpos( $block['blockName'], 'boldblocks/' ) === 0 ) {
1851 if ( isset( $block_instance->block_type->supports['parentLayoutType'] ) && 'carousel' === $block_instance->block_type->supports['parentLayoutType'] ) {
1852 $GLOBALS['boldblocks_carousel_loaded'] = true;
1853 }
1854 }
1855
1856 return $block_content;
1857 }
1858
1859 /**
1860 * Whether to load the carousel scripts for core query block.
1861 *
1862 * @param array $block
1863 * @param WP_Block $block_instance
1864 * @return boolean
1865 */
1866 private function is_enqueue_carousel_script_for_query_loop( $block, $block_instance ) {
1867 if ( 'carousel' === ( $block['attrs']['displayLayout']['type'] ?? '' ) ) {
1868 return true;
1869 }
1870
1871 if ( $block_instance->block_type->api_version >= 3 ) {
1872 $post_template = $this->the_plugin_instance->get_component( CustomStyle::class )->find_nested_block( $block_instance, 'core/post-template' );
1873
1874 if ( $post_template ) {
1875 if ( 'carousel' === ( $post_template->attributes['boldblocks']['layout']['type'] ?? '' ) ) {
1876 return true;
1877 }
1878 }
1879 }
1880
1881 return false;
1882 }
1883
1884 /**
1885 * Register custom rest fields for easier getting, updating data.
1886 *
1887 * @return void
1888 */
1889 public function register_rest_fields() {
1890 register_rest_field(
1891 $this->post_type,
1892 'keywords',
1893 array(
1894 'get_callback' => function ( $params ) {
1895 $keywords = wp_list_pluck( wp_get_object_terms( $params['id'], 'boldblocks_block_keywords' ), 'name' );
1896
1897 return \implode( ',', $keywords );
1898 },
1899 'update_callback' => function ( $value, $post ) {
1900 wp_set_post_terms( $post->ID, $value, 'boldblocks_block_keywords' );
1901 },
1902 'schema' => array(
1903 'type' => 'string',
1904 ),
1905 )
1906 );
1907 }
1908
1909 /**
1910 * Render the inline script as inline script module.
1911 *
1912 * @param array $attributes
1913 * @return array
1914 */
1915 public function add_inline_script_module( $attributes ) {
1916 if ( $attributes['id'] ?? false ) {
1917 if ( strpos( $attributes['id'], 'cbb-script-module-' ) === 0 ) {
1918 $attributes['type'] = 'module';
1919 }
1920 }
1921
1922 return $attributes;
1923 }
1924
1925 /**
1926 * Hide the block on the frontend.
1927 *
1928 * @param string $block_content
1929 * @param array $block
1930 * @param WP_Block $block_instance
1931 * @return string
1932 */
1933 public function hide_on_frontend( $block_content, $block, $block_instance ) {
1934 // Ignore admin side.
1935 if ( is_admin() ) {
1936 return $block_content;
1937 }
1938
1939 if ( $block_instance->block_type->supports['hideOnFrontend'] ?? false ) {
1940 return null;
1941 }
1942
1943 return $block_content;
1944 }
1945
1946 /**
1947 * Enqueue scripts for custom blocks when editing them
1948 *
1949 * @param string $hook_suffix
1950 * @return void
1951 */
1952 public function enqueue_custom_block_scripts( $hook_suffix ) {
1953 global $post;
1954 $screen = get_current_screen();
1955 if ( 'post.php' === $hook_suffix && $this->post_type === $screen->post_type ) {
1956 $custom_style_handler = $this->the_plugin_instance->get_component( CustomStyle::class );
1957 // Styles.
1958 $external_styles = get_post_meta( $post->ID, 'boldblocks_block_external_styles', true );
1959 $custom_styles = get_post_meta( $post->ID, 'boldblocks_block_custom_styles', true );
1960
1961 $block_class = '.wp-block-post-content';
1962
1963 if ( ! empty( $external_styles ) ) {
1964 $external_styles = array_filter(
1965 $external_styles,
1966 function ( $style ) {
1967 return $style['handle'] ?? false;
1968 }
1969 );
1970
1971 foreach ( $external_styles as $style ) {
1972 if ( $style['deps'] ?? false ) {
1973 $deps = $this->refine_style_handle( $style['deps'], $is_backend );
1974 $deps = \explode( ',', $deps );
1975 } else {
1976 $deps = [];
1977 }
1978 $version = $style['version'] ?? null;
1979 if ( $version ) {
1980 if ( 'WP' === $version ) {
1981 $version = '';
1982 } elseif ( 'CBB' === $version ) {
1983 $version = BOLDBLOCKS_CBB_VERSION;
1984 }
1985 }
1986 // Don't load external resources on admin side.
1987 if ( ! ( $style['src'] ?? '' ) ) {
1988 // Add 'edit-' prefix to separate it from the handle registered by other hook.
1989 wp_register_style( 'edit-' . $style['handle'], $style['src'] ?? '', $deps, $version, $style['media'] ?? 'all' );
1990 }
1991 }
1992 }
1993
1994 if ( ! empty( $custom_styles ) ) {
1995 $custom_styles = array_filter(
1996 $custom_styles,
1997 function ( $style ) {
1998 return ! empty( $style['value'] ) && 'view' !== ( $style['type'] ?? '' );
1999 }
2000 );
2001
2002 $dynamic_style = '';
2003 foreach ( $custom_styles as $style ) {
2004 $dynamic_style .= $custom_style_handler->refine_custom_value( $style['value'], [ 'selector' => $block_class ], 'CSS' );
2005 }
2006
2007 if ( $dynamic_style ) {
2008 $dynamic_style .= '.wp-block-post-content{position:relative!important;top:unset!important;right:unset!important;bottom:unset!important;left:unset!important;display:block!important;width:auto!important;max-width:none!important;height:auto!important;max-height:none!important;}.is-selected,.has-child-selected,.has-child-selected * {animation:none!important;}';
2009 $handle = 'edit-' . $this->custom_blocks_handle;
2010 if ( ! wp_style_is( $handle, 'registered' ) ) {
2011 // phpcs:ignore WordPress.WP.EnqueuedResourceParameters.MissingVersion
2012 wp_register_style( $handle, '' );
2013 }
2014
2015 wp_add_inline_style( $handle, $dynamic_style );
2016 wp_enqueue_style( $handle );
2017 }
2018 }
2019 }
2020 }
2021
2022 /**
2023 * Renders the 'readonly' block on the server.
2024 *
2025 * @param array $attributes Block attributes.
2026 * @param string $content Block default content.
2027 * @param WP_Block $block Block instance.
2028 * @return string Returns the block content on the frontend.
2029 */
2030 public function render_readonly_block( $attributes, $content, $block ) {
2031 $raw_block_content = $block->block_type->supports['block_content'] ?? '';
2032
2033 // Ignore empty block.
2034 if ( empty( $raw_block_content ) ) {
2035 return $content;
2036 }
2037
2038 if ( ! empty( $attributes['innerContents'] ) ) {
2039 // Convert to bindings.
2040 $raw_block_content = str_replace( 'cbbBindings', 'bindings', $raw_block_content );
2041
2042 // Handle embeds.
2043 global $wp_embed;
2044 $raw_block_content = $wp_embed->run_shortcode( $raw_block_content );
2045 $raw_block_content = $wp_embed->autoembed( $raw_block_content );
2046
2047 $filter_block_context = static function ( $context ) use ( $attributes ) {
2048 $context['cbb/block-overrides'] = $attributes['innerContents'];
2049 return $context;
2050 };
2051 add_filter( 'render_block_context', $filter_block_context, 1 );
2052
2053 $block_content = do_blocks( $raw_block_content );
2054
2055 remove_filter( 'render_block_context', $filter_block_context, 1 );
2056 } else {
2057 $block_content = apply_filters( 'the_content', $raw_block_content );
2058 }
2059
2060 if ( $block_content ) {
2061 $wrapper_content = trim( $block->parsed_block['innerHTML'] ?? '' );
2062 if ( $wrapper_content ) {
2063 $closing_div = substr( $wrapper_content, -6 );
2064
2065 if ( $closing_div === '</div>' ) {
2066 $opening_div = substr( $wrapper_content, 0, strlen( $wrapper_content ) - 6 );
2067 $block_content = $opening_div . $block_content . $closing_div;
2068
2069 return $block_content;
2070 }
2071 }
2072 }
2073
2074 return $content;
2075 }
2076
2077 /**
2078 * Customize Query Loop
2079 *
2080 * @return void
2081 */
2082 public function query_loop_extended_filters_and_sorting() {
2083 // Get public post types.
2084 $post_types = get_post_types(
2085 [
2086 'public' => true,
2087 'show_in_rest' => true,
2088 ]
2089 );
2090
2091 // For backend.
2092 foreach ( $post_types as $post_type ) {
2093 add_filter( 'rest_' . $post_type . '_collection_params', [ $this, 'query_loop_support_advanced_sorting' ], 10 );
2094 add_filter( 'rest_' . $post_type . '_query', [ $this, 'query_loop_add_custom_query_params' ], 10, 2 );
2095 }
2096
2097 // For frontend.
2098 add_filter( 'query_loop_block_query_vars', [ $this, 'query_loop_customize_frontend' ], 10, 2 );
2099
2100 // Add custom query vars.
2101 add_filter( 'query_vars', [ $this, 'query_loop_query_vars' ], 1 );
2102
2103 // Alter the result of query loop.
2104 add_filter( 'the_posts', [ $this, 'query_loop_the_posts' ], 10, 2 );
2105 }
2106
2107 /**
2108 * Support advanced sorting for the Query Loop block.
2109 *
2110 * @param array $query_params
2111 * @return array
2112 */
2113 public function query_loop_support_advanced_sorting( $query_params ) {
2114 $query_params['orderby']['enum'][] = 'meta_value';
2115 $query_params['orderby']['enum'][] = 'meta_value_num';
2116 $query_params['orderby']['enum'][] = 'menu_order';
2117 $query_params['orderby']['enum'][] = 'rand';
2118
2119 return $query_params;
2120 }
2121
2122 /**
2123 * Customize query args for frontend.
2124 *
2125 * @param array $query_args
2126 * @param WP_Block $block
2127 * @return array
2128 */
2129 public function query_loop_customize_frontend( $query_args, $block ) {
2130 $query_context = $block->context['query'] ?? [];
2131 $queried_object = get_queried_object();
2132
2133 return $this->query_loop_alter_query_params( $query_args, $query_context['cbb'] ?? [], $query_context, $queried_object );
2134 }
2135
2136 /**
2137 * Add custom query parameters to the Query Loop.
2138 *
2139 * @param array $query_args
2140 * @param WP_REST_Request $request
2141 * @return array
2142 */
2143 public function query_loop_add_custom_query_params( $query_args, $request ) {
2144 return $this->query_loop_alter_query_params( $query_args, $request->get_param( 'cbb' ) ?? [] );
2145 }
2146
2147 /**
2148 * Change query args by CBB.
2149 *
2150 * @param array $query_args
2151 * @param array $cbb_params
2152 * @param mixed $query_context
2153 * @param mixed $queried_object
2154 * @return array
2155 */
2156 private function query_loop_alter_query_params( $query_args, $cbb_params, $query_context = null, $queried_object = null ) {
2157 $cbb_args = [];
2158 if ( $cbb_params && is_array( $cbb_params ) ) {
2159 // It's a cbb query.
2160 $cbb_args = [ 'cbb' => $cbb_params ];
2161
2162 // Query additional post types.
2163 $post_type_args = $this->query_loop_filter_by_post_types( $cbb_params['postTypes'] ?? '' );
2164 if ( $post_type_args ) {
2165 $post_type = (array) ( $query_args['post_type'] ?? 'post' );
2166 $post_types = array_merge( $post_type, $post_type_args );
2167 $cbb_args['post_type'] = $post_types;
2168 // Set query args here for the context filter below.
2169 $query_args['post_type'] = $post_types;
2170 }
2171
2172 // Query by custom post ids.
2173 $post_in_args = $this->query_loop_filter_by_post_ids( $cbb_params['include'] ?? '' );
2174 if ( $post_in_args ) {
2175 $cbb_args = array_merge( $cbb_args, $post_in_args );
2176 }
2177
2178 // Query by meta fields.
2179 $meta_query_args = $this->query_loop_filter_by_meta_queries( $cbb_params );
2180 if ( $meta_query_args ) {
2181 $cbb_args = array_merge( $cbb_args, $meta_query_args );
2182 }
2183
2184 // Query by parent.
2185 $parent_args = $this->query_loop_filter_by_parent( $cbb_params['parent'] ?? '' );
2186 if ( $parent_args ) {
2187 $cbb_args = array_merge( $cbb_args, $parent_args );
2188 }
2189
2190 // Query by context.
2191 if ( $queried_object && $queried_object instanceof \WP_Post ) {
2192 $query_post_type = $query_args['post_type'] ?? '';
2193 if ( $queried_object->post_type === $query_post_type || ( is_array( $query_post_type ) && in_array( $queried_object->post_type, $query_post_type, true ) ) ) {
2194 $context_args = $this->query_loop_filter_by_context( $cbb_params, $queried_object );
2195
2196 if ( $context_args['post__not_in'] ?? false ) {
2197 // phpcs:ignore WordPressVIPMinimum.Performance.WPQueryParams.PostNotIn_post__not_in
2198 $cbb_args['post__not_in'] = $context_args['post__not_in'];
2199 }
2200
2201 if ( $context_args['tax_query'] ?? false ) {
2202 if ( ( $query_args['tax_query'] ?? false ) && is_array( $query_args['tax_query'] ) ) {
2203 // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query
2204 $cbb_args['tax_query'] = array_merge( $query_args['tax_query'], [ $context_args['tax_query'] ] );
2205 } else {
2206 // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query
2207 $cbb_args['tax_query'] = $context_args['tax_query'];
2208 }
2209 }
2210 }
2211
2212 // ignoreStickyPosts is depreated since WP 6.8.
2213 // Ignore sticky posts not exlude them.
2214 if ( ( $cbb_params['ignoreStickyPosts'] ?? false ) && ( 'exclude' === $query_context['sticky'] ?? '' ) ) {
2215 $cbb_args['ignore_sticky_posts'] = true;
2216 $sticky = get_option( 'sticky_posts' );
2217 if ( ! empty( $sticky ) && ! empty( $query_args['post__not_in'] ) ) {
2218 // Remove sticky from the post__not_in list.
2219 // phpcs:ignore WordPressVIPMinimum.Performance.WPQueryParams.PostNotIn_post__not_in
2220 $query_args['post__not_in'] = array_diff( $query_args['post__not_in'], $sticky );
2221 }
2222 }
2223 }
2224
2225 // Custom sorting.
2226 $orderby = [];
2227 // phpcs:ignore WordPress.Security.NonceVerification.Recommended
2228 if ( ( $cbb_params['orderbyFromQueryString'] ?? false ) && isset( $_GET['orderby'] ) ) {
2229 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.NonceVerification.Recommended
2230 $orderby = wp_unslash( $_GET['orderby'] );
2231 // orderby will be sanitized below.
2232 if ( $orderby ) {
2233 if ( ! is_array( $orderby ) ) {
2234 $orderby = [ $orderby ];
2235 }
2236
2237 $orderby = array_filter(
2238 array_map(
2239 function ( $item ) {
2240 return $item ? sanitize_text_field( $item ) : false;
2241 },
2242 $orderby
2243 )
2244 );
2245 }
2246 }
2247
2248 $sort_args = $this->query_loop_custom_sort( array_merge( $orderby, $cbb_params['sorting'] ?? [] ) );
2249 if ( $sort_args ) {
2250 $cbb_args = array_merge( $cbb_args, $sort_args );
2251
2252 if ( isset( $query_args['order'] ) ) {
2253 unset( $query_args['order'] );
2254 }
2255 }
2256
2257 // Combine post__not_in value.
2258 // phpcs:ignore WordPressVIPMinimum.Performance.WPQueryParams.PostNotIn_post__not_in
2259 $cbb_args['post__not_in'] = array_merge( $query_args['post__not_in'], $cbb_args['post__not_in'] ?? [] );
2260
2261 // Don't need pagination.
2262 if ( $cbb_params['disablePagination'] ?? false ) {
2263 $cbb_args['no_found_rows'] = true;
2264 }
2265 }
2266
2267 // Allow third-party to alter the final result.
2268 $query_args = apply_filters( 'cbb_query_loop_block_query_vars', array_merge( $query_args, $cbb_args ), $query_args, $cbb_params, $query_context, $queried_object );
2269
2270 return $query_args;
2271 }
2272
2273 /**
2274 * Filter by additional post types.
2275 *
2276 * @param string $post_types
2277 * @return array
2278 */
2279 private function query_loop_filter_by_post_types( $post_types ) {
2280 $post_types_args = [];
2281 $post_types = trim( $post_types );
2282 if ( $post_types ) {
2283 $post_types_args = explode( ',', $post_types );
2284 }
2285
2286 return $post_types_args;
2287 }
2288
2289 /**
2290 * Filter by post ids.
2291 *
2292 * @param string $include
2293 * @return array
2294 */
2295 private function query_loop_filter_by_post_ids( $include ) {
2296 $post_in_args = [];
2297 if ( $include ) {
2298 $post_in = explode( ',', $include );
2299 if ( $post_in ) {
2300 $post_in_args['post__in'] = $post_in;
2301 $post_in_args['orderby'] = 'post__in';
2302 $post_in_args['ignore_sticky_posts'] = true;
2303 }
2304 }
2305
2306 return $post_in_args;
2307 }
2308
2309 /**
2310 * Filter by parent.
2311 *
2312 * @param string $parent
2313 * @return array
2314 */
2315 private function query_loop_filter_by_parent( $parent ) {
2316 $parent_args = [];
2317 if ( $parent ) {
2318 $parent_ids = explode( ',', $parent );
2319 if ( count( $parent_ids ) > 1 ) {
2320 $parent_args['post_parent__in'] = $parent_ids;
2321 } else {
2322 $parent_args['post_parent'] = $parent;
2323 }
2324 }
2325
2326 return $parent_args;
2327 }
2328
2329 /**
2330 * Filter by meta queries
2331 *
2332 * @param array $params
2333 * @return array
2334 */
2335 private function query_loop_filter_by_meta_queries( $params ) {
2336 $meta_query_args = [];
2337 // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
2338 $queries = $params['metaQuery']['queries'] ?? [];
2339 if ( $queries && count( $queries ) > 0 ) {
2340 $queries = array_map(
2341 function ( $query ) {
2342 $refined_query = [
2343 'type' => strtoupper( $query['type'] ?? 'char' ),
2344 'compare' => $query['compare'] ?? '=',
2345 'key' => $query['key'] ?? '',
2346 'value' => $query['value'] ?? '',
2347 ];
2348
2349 if ( 'NUMERIC' === $refined_query['type'] ) {
2350 $refined_query['type'] = 'DECIMAL(10,3)';
2351 }
2352
2353 // No key.
2354 if ( ! $refined_query['key'] ) {
2355 return false;
2356 }
2357
2358 // Don't need a value.
2359 if ( in_array( $refined_query['compare'], [ 'EXISTS', 'NOT EXISTS' ], true ) ) {
2360 unset( $refined_query['value'] );
2361
2362 return $refined_query;
2363 }
2364
2365 $compare = $refined_query['compare'];
2366 $refined_value = $refined_query['value'];
2367
2368 // Get query string.
2369 $query_string = $query['query_string'] ?? '';
2370 // phpcs:ignore WordPress.Security.NonceVerification.Recommended
2371 if ( $query_string && isset( $_GET[ $query_string ] ) ) {
2372 // phpcs:ignore WordPress.Security.NonceVerification.Recommended
2373 $refined_value = sanitize_text_field( wp_unslash( $_GET[ $query_string ] ) );
2374 }
2375
2376 // No value.
2377 if ( $refined_value === '' ) {
2378 return false;
2379 }
2380
2381 if ( in_array( $compare, [ 'IN', 'NOT IN', 'BETWEEN', 'NOT BETWEEN' ], true ) ) {
2382 $refined_value = preg_split( '/,\s*(?=(?:[^"]*"[^"]*")*[^"]*$)/', $refined_value, -1, PREG_SPLIT_NO_EMPTY );
2383 $refined_value = array_filter(
2384 array_map(
2385 function ( $value ) {
2386 return trim( $value, '"' );
2387 },
2388 $refined_value
2389 )
2390 );
2391 }
2392
2393 if ( 'DATE' === $refined_query['type'] ) {
2394 $format = ! empty( $query['date_format'] ) ? $query['date_format'] : 'Y-m-d';
2395
2396 if ( is_array( $refined_value ) ) {
2397 $refined_value = array_filter(
2398 array_map(
2399 function ( $value ) {
2400 $formated_value = strtotime( $value );
2401
2402 // No valid date string.
2403 if ( ! $formated_value ) {
2404 return false;
2405 }
2406
2407 if ( 'timestamp' !== $format ) {
2408 $formated_value = gmdate( $format, $formated_value );
2409 }
2410
2411 return $formated_value;
2412 },
2413 $refined_value
2414 )
2415 );
2416 } else {
2417 $refined_value = strtotime( $refined_value );
2418
2419 if ( 'timestamp' !== $format ) {
2420 $refined_value = gmdate( $format, $refined_value );
2421 }
2422 }
2423
2424 // No valid value.
2425 if ( ! $refined_value ) {
2426 return false;
2427 }
2428
2429 if ( 'timestamp' === $format ) {
2430 $refined_query['type'] = 'NUMERIC';
2431 }
2432 }
2433
2434 // Update value.
2435 $refined_query['value'] = $refined_value;
2436
2437 return $refined_query;
2438 },
2439 $queries
2440 );
2441
2442 $queries = array_filter( $queries );
2443
2444 if ( count( $queries ) > 0 ) {
2445 // Maximize 5 items.
2446 $queries = array_slice( $queries, 0, 5 );
2447
2448 $refined_queries = [];
2449 $instances = [];
2450 foreach ( $queries as $query ) {
2451 $key = $query['key'];
2452 if ( ! isset( $refined_queries[ $key ] ) ) {
2453 $refined_queries[ $key ] = $query;
2454 $instances[ $key ] = 1;
2455 } else {
2456 $refined_queries[ $key . '_' . $instances[ $key ] ] = $query;
2457 $instances[ $key ] = absint( $instances[ $key ] ) + 1;
2458 }
2459 }
2460
2461 // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
2462 $meta_query_args['meta_query'] = $refined_queries;
2463 if ( count( $queries ) > 1 ) {
2464 // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
2465 $meta_query_args['meta_query']['relation'] = $params['metaQuery']['relation'] ?? 'AND';
2466 }
2467 }
2468 }
2469
2470 return $meta_query_args;
2471 }
2472
2473 /**
2474 * Filter by context.
2475 *
2476 * @param array $params
2477 * @return array
2478 */
2479 private function query_loop_filter_by_context( $params, $post ) {
2480 $context_args = [];
2481
2482 if ( $params['ignoreCurrentPost'] ?? false ) {
2483 // phpcs:ignore WordPressVIPMinimum.Performance.WPQueryParams.PostNotIn_post__not_in
2484 $context_args['post__not_in'] = [ $post->ID ];
2485 }
2486
2487 if ( $params['queryRelatedPosts'] ?? false ) {
2488 $taxonomies = \get_object_taxonomies( $post, 'names' );
2489
2490 $tax_queries = [];
2491 foreach ( $taxonomies as $taxonomy ) {
2492 $terms = \get_the_terms( $post->ID, $taxonomy );
2493 if ( empty( $terms ) ) {
2494 continue;
2495 }
2496 $term_ids = \wp_list_pluck( $terms, 'term_id' );
2497 $tax_queries[] = array(
2498 'taxonomy' => $taxonomy,
2499 'terms' => $term_ids,
2500 );
2501 }
2502
2503 if ( count( $tax_queries ) > 1 ) {
2504 $tax_queries['relation'] = 'OR';
2505 }
2506
2507 if ( $tax_queries ) {
2508 // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query
2509 $context_args['tax_query'] = $tax_queries;
2510 }
2511 }
2512
2513 return $context_args;
2514 }
2515
2516 /**
2517 * Custom sorting.
2518 *
2519 * @param array $sorting
2520 * @return array
2521 */
2522 private function query_loop_custom_sort( $sorting ) {
2523 $sort_args = [];
2524
2525 if ( $sorting && is_array( $sorting ) ) {
2526 if ( 'rand' === $sorting[0] ) {
2527 $sort_args['orderby'] = 'rand';
2528 } else {
2529 $order_by = [];
2530 foreach ( $sorting as $item ) {
2531 if ( ! $item || 'rand' === $item ) {
2532 continue;
2533 }
2534
2535 $item_array = explode( '/', $item );
2536
2537 if ( count( $item_array ) !== 2 ) {
2538 continue;
2539 }
2540
2541 if ( isset( $order_by[ $item_array[0] ] ) || ! in_array( strtoupper( $item_array[1] ), [ 'ASC', 'DESC' ], true ) ) {
2542 continue;
2543 }
2544
2545 $order_by[ $item_array[0] ] = strtoupper( $item_array[1] );
2546 }
2547
2548 if ( $order_by ) {
2549 $sort_args['orderby'] = $order_by;
2550 }
2551 }
2552 }
2553
2554 return $sort_args;
2555 }
2556
2557 /**
2558 * Add custom query vars.
2559 *
2560 * @param array $vars
2561 * @return array
2562 */
2563 public function query_loop_query_vars( $vars ) {
2564 $vars[] = 'cbb';
2565
2566 return $vars;
2567 }
2568
2569 /**
2570 * Alter the result of the query loop
2571 *
2572 * @param array $posts
2573 * @param WP_Query $query
2574 * @return array
2575 */
2576 public function query_loop_the_posts( $posts, $query ) {
2577 if ( $query->query_vars['cbb'] ?? false ) {
2578 $cbb_args = $query->query_vars['cbb'] ?? [];
2579 $posts_per_page = $query->query_vars['posts_per_page'] ?? 1;
2580 $found_posts = $query->found_posts;
2581
2582 if ( ( $cbb_args['queryFallbackPosts'] ?? false ) && $found_posts < $posts_per_page ) {
2583 $ignore_posts = [];
2584 if ( $found_posts > 0 ) {
2585 $ignore_posts = wp_list_pluck( $query->posts, 'ID' );
2586 }
2587
2588 $fallback_args = [
2589 'post_type' => $query->query_vars['post_type'] ?? 'any',
2590 'post_status' => 'publish',
2591 'posts_per_page' => $posts_per_page - $found_posts,
2592 'ignore_sticky_posts' => 1,
2593 // phpcs:ignore WordPressVIPMinimum.Performance.WPQueryParams.PostNotIn_post__not_in
2594 'post__not_in' => array_merge( $query->query_vars['post__not_in'] ?? [], $ignore_posts ),
2595 'no_found_rows' => true,
2596 ];
2597
2598 $fallback_posts = get_posts( $fallback_args );
2599
2600 if ( $fallback_posts ) {
2601 $posts = array_merge( $posts, $fallback_posts );
2602 }
2603 }
2604 }
2605
2606 return $posts;
2607 }
2608
2609 /**
2610 * Get the current post
2611 *
2612 * @return string
2613 */
2614 public function get_current_post() {
2615 global $pagenow;
2616 // phpcs:ignore WordPress.Security.NonceVerification.Recommended
2617 $post_id = 'post.php' === $pagenow && isset( $_GET['post'] ) ? absint( $_GET['post'] ) : 0;
2618 $post = $post_id ? get_post( $post_id ) : false;
2619
2620 return $post instanceof \WP_Post ? $post : false;
2621 }
2622
2623 /**
2624 * Get max items
2625 */
2626 public function get_max_items() {
2627 return apply_filters( 'cbb_get_max_items', $this->max_items, $this->post_type );
2628 }
2629 }
2630 endif;
2631