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

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