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

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