PluginProbe
Content Blocks Builder – Create blocks, repeater blocks with carousel, grid, popup layouts / 2.7.9
Content Blocks Builder – Create blocks, repeater blocks with carousel, grid, popup layouts v2.7.9
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.7.9, at includes/custom-blocks.php

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