false, 'render_callback' => false, 'enqueue_style' => false, 'enqueue_script' => false, 'enqueue_assets' => false, 'post_types' => array(), 'uses_context' => array(), 'supports' => array(), 'attributes' => array(), 'acf_block_version' => 2, 'api_version' => 2, 'validate' => true, 'validate_on_load' => true, 'use_post_meta' => false, ) ); // Add user provided attributes to ACF's required defaults. $settings['attributes'] = wp_parse_args( acf_get_block_type_default_attributes( $metadata ), $settings['attributes'] ); // Add default ACF 'supports' settings. $settings['supports'] = wp_parse_args( $settings['supports'], array( 'align' => true, 'html' => false, 'mode' => true, 'jsx' => true, 'multiple' => true, ) ); // Add default ACF 'uses_context' settings. $settings['uses_context'] = array_values( array_unique( array_merge( $settings['uses_context'], array( 'postId', 'postType', ) ) ) ); // Map custom ACF properties from the ACF key, with localization. $property_mappings = array( 'renderCallback' => 'render_callback', 'renderTemplate' => 'render_template', 'mode' => 'mode', 'blockVersion' => 'acf_block_version', 'postTypes' => 'post_types', 'validate' => 'validate', 'validateOnLoad' => 'validate_on_load', 'usePostMeta' => 'use_post_meta', ); $textdomain = ! empty( $metadata['textdomain'] ) ? $metadata['textdomain'] : 'acf'; $i18n_schema = get_block_metadata_i18n_schema(); foreach ( $property_mappings as $key => $mapped_key ) { if ( isset( $metadata['acf'][ $key ] ) ) { unset( $settings[ $key ] ); $settings[ $mapped_key ] = $metadata['acf'][ $key ]; if ( $textdomain && isset( $i18n_schema->$key ) ) { $settings[ $mapped_key ] = translate_settings_using_i18n_schema( $i18n_schema->$key, $settings[ $key ], $textdomain ); } } } // Add the block name and registration path to settings. $settings['name'] = $metadata['name']; $settings['path'] = dirname( $metadata['file'] ); // Prevent blocks that usePostMeta from being nested or saving multiple. if ( ! empty( $settings['use_post_meta'] ) ) { $settings['parent'] = array( 'core/post-content' ); $settings['supports']['multiple'] = false; } acf_get_store( 'block-types' )->set( $metadata['name'], $settings ); add_action( 'enqueue_block_editor_assets', 'acf_enqueue_block_assets' ); // Ensure our render callback is used. $settings['render_callback'] = 'acf_render_block_callback'; return $settings; } /** * Check if a block.json block is an ACF block. * * @since 6.0.0 * * @param array $metadata The raw block metadata array. * @return boolean */ function acf_is_acf_block_json( $metadata ) { return ( isset( $metadata['acf'] ) && $metadata['acf'] ); } /** * Registers a block type. * * @date 18/2/19 * @since 5.8.0 * * @param array $block The block settings. * @return (array|false) */ function acf_register_block_type( $block ) { // Validate block type settings. $block = acf_validate_block_type( $block ); /** * Filters the arguments for registering a block type. * * @since 5.8.9 * * @param array $block The array of arguments for registering a block type. */ $block = apply_filters( 'acf/register_block_type_args', $block ); // Require name. if ( ! $block['name'] ) { $message = __( 'Block type name is required.', 'acf' ); _doing_it_wrong( __FUNCTION__, $message, '5.8.0' ); //phpcs:ignore -- escape not required. return false; } // Bail early if already exists. if ( acf_has_block_type( $block['name'] ) ) { /* translators: The name of the block type */ $message = sprintf( __( 'Block type "%s" is already registered.', 'acf' ), $block['name'] ); _doing_it_wrong( __FUNCTION__, $message, '5.8.0' ); //phpcs:ignore -- escape not required. return false; } // Set ACF required attributes. $block['attributes'] = acf_get_block_type_default_attributes( $block ); if ( ! isset( $block['api_version'] ) ) { $block['api_version'] = 2; } if ( ! isset( $block['acf_block_version'] ) ) { $block['acf_block_version'] = 1; } // Add to storage. acf_get_store( 'block-types' )->set( $block['name'], $block ); // Overwrite callback for WordPress registration. $block['render_callback'] = 'acf_render_block_callback'; // Register block type in WP. if ( function_exists( 'register_block_type' ) ) { register_block_type( $block['name'], $block ); } // Register action. add_action( 'enqueue_block_editor_assets', 'acf_enqueue_block_assets' ); // Return block. return $block; } /** * See acf_register_block_type(). * * @date 18/2/19 * @since 5.7.12 * * @param array $block The block settings. * @return (array|false) */ function acf_register_block( $block ) { return acf_register_block_type( $block ); } /** * Returns true if a block type exists for the given name. * * @since 5.7.12 * * @param string $name The block type name. * @return boolean */ function acf_has_block_type( $name ) { return acf_get_store( 'block-types' )->has( $name ); } /** * Returns an array of all registered block types. * * @since 5.7.12 * * @return array */ function acf_get_block_types() { return acf_get_store( 'block-types' )->get(); } /** * Returns a block type for the given name. * * @since 5.7.12 * * @param string $name The block type name. * @return (array|null) */ function acf_get_block_type( $name ) { return acf_get_store( 'block-types' )->get( $name ); } /** * Removes a block type for the given name. * * @since 5.7.12 * * @param string $name The block type name. * @return void */ function acf_remove_block_type( $name ) { acf_get_store( 'block-types' )->remove( $name ); } /** * Returns an array of default attribute settings for a block type. * * @date 19/11/18 * @since 5.8.0 * * @param array $block_type A block configuration array. * @return array */ function acf_get_block_type_default_attributes( $block_type ) { $attributes = array( 'name' => array( 'type' => 'string', 'default' => '', ), 'data' => array( 'type' => 'object', 'default' => array(), ), 'align' => array( 'type' => 'string', 'default' => '', ), 'mode' => array( 'type' => 'string', 'default' => '', ), ); foreach ( acf_get_block_back_compat_attribute_key_array() as $new => $old ) { if ( isset( $block_type['supports'][ $old ] ) ) { $block_type['supports'][ $new ] = $block_type['supports'][ $old ]; unset( $block_type['supports'][ $old ] ); } } if ( ! empty( $block_type['supports']['alignText'] ) ) { $attributes['alignText'] = array( 'type' => 'string', 'default' => '', ); } if ( ! empty( $block_type['supports']['alignContent'] ) ) { $attributes['alignContent'] = array( 'type' => 'string', 'default' => '', ); } if ( ! empty( $block_type['supports']['fullHeight'] ) ) { $attributes['fullHeight'] = array( 'type' => 'boolean', 'default' => '', ); } // For each of ACF's block attributes, check if the user's block attributes contains a default value we should use. if ( isset( $block_type['attributes'] ) && is_array( $block_type['attributes'] ) ) { foreach ( array_keys( $attributes ) as $key ) { if ( isset( $block_type['attributes'][ $key ] ) && is_array( $block_type['attributes'][ $key ] ) && isset( $block_type['attributes'][ $key ]['default'] ) ) { $attributes[ $key ]['default'] = $block_type['attributes'][ $key ]['default']; } } } return $attributes; } /** * Validates a block type ensuring all settings exist. * * @since 5.8.0 * * @param array $block The block settings. * @return array */ function acf_validate_block_type( $block ) { // Add default settings. $block = wp_parse_args( $block, array( 'name' => '', 'title' => '', 'description' => '', 'category' => 'common', 'icon' => '', 'mode' => 'preview', 'keywords' => array(), 'supports' => array(), 'post_types' => array(), 'uses_context' => array(), 'render_template' => false, 'render_callback' => false, 'enqueue_style' => false, 'enqueue_script' => false, 'enqueue_assets' => false, ) ); // Generate name with prefix. if ( $block['name'] ) { $block['name'] = 'acf/' . acf_slugify( $block['name'] ); } // Add default 'supports' settings. $block['supports'] = wp_parse_args( $block['supports'], array( 'align' => true, 'html' => false, 'mode' => true, ) ); // Add default 'uses_context' settings. $block['uses_context'] = wp_parse_args( $block['uses_context'], array( 'postId', 'postType', ) ); // Correct "Experimental" flags. if ( isset( $block['supports']['__experimental_jsx'] ) ) { $block['supports']['jsx'] = $block['supports']['__experimental_jsx']; } // Return block. return $block; } /** * Prepares a block for use in render_callback by merging in all settings and attributes. * * @since 5.8.0 * * @param array $block The block props. * @return array|boolean */ function acf_prepare_block( $block ) { // Bail early if no name. if ( ! isset( $block['name'] ) ) { return false; } // Ensure a block ID is always prefixed with `block_` for meta. $block['id'] = acf_ensure_block_id_prefix( $block['id'] ); // Get block type and return false if doesn't exist. $block_type = acf_get_block_type( $block['name'] ); if ( ! $block_type ) { return false; } // Prevent protected attributes being overridden. $protected = array( 'render_template', 'render_callback', 'enqueue_script', 'enqueue_style', 'enqueue_assets', 'post_types', 'use_post_meta', ); $block = array_diff_key( $block, array_flip( $protected ) ); // Generate default attributes. $attributes = array(); foreach ( acf_get_block_type_default_attributes( $block_type ) as $k => $v ) { $attributes[ $k ] = $v['default']; } // Merge together arrays in order of least to most specific. $block = array_merge( $block_type, $attributes, $block ); // Add backward compatibility attributes. $block = acf_add_back_compat_attributes( $block ); // Return block. return $block; } /** * Add backwards compatible attribute values. * * @since 6.0.0 * * @param array $block The original block. * @return array Modified block array with backwards compatibility attributes. */ function acf_add_back_compat_attributes( $block ) { foreach ( acf_get_block_back_compat_attribute_key_array() as $new => $old ) { if ( ! empty( $block[ $new ] ) || ( isset( $block[ $new ] ) && ! isset( $block[ $old ] ) ) ) { $block[ $old ] = $block[ $new ]; } } return $block; } /** * Get back compat new values and old values. * * @since 6.0.0 * * @return array back compat key array. */ function acf_get_block_back_compat_attribute_key_array() { return array( 'fullHeight' => 'full_height', 'alignText' => 'align_text', 'alignContent' => 'align_content', ); } /** * The render callback for all ACF blocks. * * @date 28/10/20 * @since 5.9.2 * * @param array $attributes The block attributes. * @param string $content The block content. * @param WP_Block $wp_block The block instance (since WP 5.5). * @return string The block HTML. */ function acf_render_block_callback( $attributes, $content = '', $wp_block = null ) { $is_preview = false; $post_id = get_the_ID(); // Set preview flag to true when rendering for the block editor. if ( is_admin() && acf_is_block_editor() ) { $is_preview = true; } // If ACF's block save method hasn't been called yet, try to initialize a default block. if ( empty( $attributes['name'] ) && ! empty( $wp_block->name ) ) { $attributes['name'] = $wp_block->name; } // Return rendered block HTML. return acf_rendered_block( $attributes, $content, $is_preview, $post_id, $wp_block ); } /** * Returns the rendered block HTML. * * @date 28/2/19 * @since 5.7.13 * * @param array $attributes The block attributes. * @param string $content The block content. * @param boolean $is_preview Whether or not the block is being rendered for editing preview. * @param integer $post_id The current post being edited or viewed. * @param WP_Block $wp_block The block instance (since WP 5.5). * @param array $context The block context array. * @param boolean $is_ajax_render Whether or not this is an ACF AJAX render. * @return string The block HTML. */ function acf_rendered_block( $attributes, $content = '', $is_preview = false, $post_id = 0, $wp_block = null, $context = false, $is_ajax_render = false ) { $mode = isset( $attributes['mode'] ) ? $attributes['mode'] : 'auto'; $form = ( 'edit' === $mode && $is_preview ); // If context is available from the WP_Block class object and we have no context of our own, use that. if ( empty( $context ) && ! empty( $wp_block->context ) ) { $context = $wp_block->context; } // Check if we need to generate a block ID. $force_new_id = false; if ( acf_block_uses_post_meta( $attributes ) && ! empty( $attributes['id'] ) && empty( $attributes['data'] ) ) { $force_new_id = true; } $attributes['id'] = acf_get_block_id( $attributes, $context, $force_new_id ); // Check if we've already got a cache of this block ID and return it to save rendering if we're in the backend. if ( $is_preview ) { $cached_block = acf_get_store( 'block-cache' )->get( $attributes['id'] ); if ( $cached_block ) { if ( $form ) { if ( $cached_block['form'] ) { return $cached_block['html']; } } elseif ( ! $cached_block['form'] ) { return $cached_block['html']; } } } ob_start(); $validation = false; if ( $form ) { // Load the block form since we're in edit mode. // Set flag for post REST cleanup of media enqueue count during preloads. acf_set_data( 'acf_did_render_block_form', true ); $block = acf_prepare_block( $attributes ); $block = acf_add_block_meta_values( $block, $post_id ); acf_setup_meta( $block['data'], $block['id'], true ); if ( ! empty( $block['validate'] ) ) { $validation = acf_get_block_validation_state( $block, false, false, true ); } $fields = acf_get_block_fields( $block ); if ( $fields ) { acf_prefix_fields( $fields, "acf-{$block['id']}" ); echo '
' . __( 'The render template for this ACF Block was not found', 'acf' ) . '
' ) ); } } /** * Returns an array of all fields for the given block. * * @date 24/10/18 * @since 5.8.0 * * @param array $block The block props. * @return array */ function acf_get_block_fields( $block ) { $fields = array(); // We need at least a block name to check. if ( empty( $block['name'] ) ) { return $fields; } // Get field groups for this block. $field_groups = acf_get_field_groups( array( 'block' => $block['name'], ) ); // Loop over results and append fields. if ( $field_groups ) { foreach ( $field_groups as $field_group ) { $fields = array_merge( $fields, acf_get_fields( $field_group ) ); } } return $fields; } /** * Enqueues and localizes block scripts and styles. * * @since 5.7.13 * * @return void */ function acf_enqueue_block_assets() { // Localize text. acf_localize_text( array( 'Switch to Edit' => __( 'Switch to Edit', 'acf' ), 'Switch to Preview' => __( 'Switch to Preview', 'acf' ), 'Change content alignment' => __( 'Change content alignment', 'acf' ), 'Error previewing block' => __( 'An error occurred when loading the preview for this block.', 'acf' ), 'Error loading block form' => __( 'An error occurred when loading the block in edit mode.', 'acf' ), /* translators: %s: Block type title */ '%s settings' => __( '%s settings', 'acf' ), ) ); // Get block types. $block_types = array_map( function ( $block ) { // Render Callback may contain a incompatible class for JSON encoding. Turn it into a boolean for the frontend. $block['render_callback'] = ! empty( $block['render_callback'] ); return $block; }, acf_get_block_types() ); // Localize data. acf_localize_data( array( 'blockTypes' => array_values( $block_types ), 'postType' => get_post_type(), ) ); // Enqueue script. $min = defined( 'ACF_DEVELOPMENT_MODE' ) && ACF_DEVELOPMENT_MODE ? '' : '.min'; $blocks_js_path = acf_get_url( "assets/build/js/pro/acf-pro-blocks{$min}.js" ); wp_enqueue_script( 'acf-blocks', $blocks_js_path, array( 'acf-input', 'wp-blocks' ), ACF_VERSION, true ); // Enqueue block assets. array_map( 'acf_enqueue_block_type_assets', $block_types ); // During the edit screen loading, WordPress renders all blocks in its own attempt to preload data. // Retrieve any cached block HTML and include this in the localized data. if ( acf_get_setting( 'preload_blocks' ) ) { $preloaded_blocks = acf_get_store( 'block-cache' )->get_data(); acf_localize_data( array( 'preloadedBlocks' => $preloaded_blocks, ) ); } } /** * Enqueues scripts and styles for a specific block type. * * @since 5.7.13 * * @param array $block_type The block type settings. * @return void */ function acf_enqueue_block_type_assets( $block_type ) { // Generate handle from name. $handle = 'block-' . acf_slugify( $block_type['name'] ); // Enqueue style. if ( $block_type['enqueue_style'] ) { wp_enqueue_style( $handle, $block_type['enqueue_style'], array(), ACF_VERSION, 'all' ); } // Enqueue script. if ( $block_type['enqueue_script'] ) { wp_enqueue_script( $handle, $block_type['enqueue_script'], array(), ACF_VERSION, true ); } // Enqueue assets callback. if ( $block_type['enqueue_assets'] && is_callable( $block_type['enqueue_assets'] ) ) { call_user_func( $block_type['enqueue_assets'], $block_type ); } } /** * Handles the ajax request for block data. * * @since 5.7.13 * * @return void */ function acf_ajax_fetch_block() { // Validate ajax request. if ( ! acf_verify_ajax() ) { wp_send_json_error(); } // Get request args. $args = acf_request_args( array( 'post_id' => 0, 'clientId' => null, 'query' => array(), ) ); // Verify capability. if ( ! empty( $args['post_id'] ) && is_numeric( $args['post_id'] ) ) { // Editing a normal post - we can verify if the user has access to that post. if ( ! acf_current_user_can_edit_post( (int) $args['post_id'] ) ) { wp_send_json_error(); } } else { // Could be editing a widget, using the site editor, etc. $render_capability = apply_filters( 'acf/blocks/render_capability', 'edit_theme_options', $args['post_id'] ); if ( ! current_user_can( $render_capability ) ) { wp_send_json_error(); } } $args['block'] = isset( $_REQUEST['block'] ) ? $_REQUEST['block'] : false; //phpcs:ignore -- requires auth; designed to contain unescaped html. $args['context'] = isset( $_REQUEST['context'] ) ? $_REQUEST['context'] : array(); //phpcs:ignore -- requires auth; designed to contain unescaped html. $block = $args['block']; $query = $args['query']; $client_id = $args['clientId']; $raw_context = $args['context']; $post_id = $args['post_id']; // Bail early if no block. if ( ! $block ) { wp_send_json_error(); } // Unslash and decode $_POST data for block and context. $block = wp_unslash( $block ); $block = json_decode( $block, true ); $context = false; if ( ! empty( $raw_context ) ) { $raw_context = wp_unslash( $raw_context ); $raw_context = json_decode( $raw_context, true ); if ( is_array( $raw_context ) ) { $context = $raw_context; // Check if a postId is set in the context, otherwise try and use it the default post_id. $post_id = isset( $context['postId'] ) ? intval( $context['postId'] ) : intval( $args['post_id'] ); } } // Check if clientId should become $block['id']. if ( empty( $block['id'] ) && ! empty( $client_id ) ) { $block['id'] = $client_id; } // Prepare block ensuring all settings and attributes exist. $block = acf_prepare_block( $block ); $block = acf_add_block_meta_values( $block, $post_id ); if ( ! $block ) { wp_send_json_error(); } // Load field defaults when first previewing a block. $first_preview = false; if ( ! empty( $query['preview'] ) && ! $block['data'] ) { $fields = acf_get_block_fields( $block ); foreach ( $fields as $field ) { $block['data'][ "_{$field['name']}" ] = $field['key']; } $first_preview = true; } // Setup postdata allowing form to load meta. acf_setup_meta( $block['data'], $block['id'], true ); // Setup main postdata for post_id. global $post; //phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited -- required for block template rendering. $post = get_post( $post_id ); setup_postdata( $post ); // Vars. $response = array( 'clientId' => $client_id ); // Check if we've recieved serialised form data $use_post_data = false; if ( ! empty( $block['data'] ) && is_array( $block['data'] ) ) { // Ensure we've got field keys posted. $valid_field_keys = array_filter( array_keys( $block['data'] ), 'acf_is_field_key' ); if ( ! empty( $valid_field_keys ) ) { $use_post_data = true; } } $query['validate'] = ( ! empty( $query['validate'] ) && ( $query['validate'] === 'true' || $query['validate'] === true ) ); if ( ! empty( $query['validate'] ) || ! empty( $block['validate'] ) ) { $response['validation'] = acf_get_block_validation_state( $block, $first_preview, $use_post_data ); } // Query form. if ( ! empty( $query['form'] ) ) { // Load fields for form. $fields = acf_get_block_fields( $block ); // Prefix field inputs to avoid multiple blocks using the same name/id attributes. acf_prefix_fields( $fields, "acf-{$block['id']}" ); if ( $fields ) { // Start Capture. ob_start(); // Render. echo '