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

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