From 18542b5cbd8ac6bf84ee7d9a8aed31e78cf2ae13 Mon Sep 17 00:00:00 2001 From: Carlos Bravo <37012961+cbravobernal@users.noreply.github.com> Date: Tue, 20 May 2025 17:14:17 +0200 Subject: [PATCH 01/20] First commit --- assets/src/js/acf.js | 1 + assets/src/js/custom-sources.js | 22 ++++++++++++++++++++++ includes/Blocks/Bindings.php | 2 -- includes/acf-pattern-functions.php | 10 ++++++++++ secure-custom-fields.php | 1 + 5 files changed, 34 insertions(+), 2 deletions(-) create mode 100644 assets/src/js/custom-sources.js create mode 100644 includes/acf-pattern-functions.php diff --git a/assets/src/js/acf.js b/assets/src/js/acf.js index 7ca4222e..215fa7c9 100644 --- a/assets/src/js/acf.js +++ b/assets/src/js/acf.js @@ -6,3 +6,4 @@ import './_acf-modal.js'; import './_acf-panel.js'; import './_acf-notice.js'; import './_acf-tooltip.js'; +import './custom-sources.js'; diff --git a/assets/src/js/custom-sources.js b/assets/src/js/custom-sources.js new file mode 100644 index 00000000..703a06f5 --- /dev/null +++ b/assets/src/js/custom-sources.js @@ -0,0 +1,22 @@ +/** + * WordPress dependencies. + */ +import { __ } from '@wordpress/i18n'; +import { registerBlockBindingsSource } from '@wordpress/blocks'; + +/** + * Register SCF Custom Fields block binding source. + * + * This allows blocks to bind to custom fields managed by + * the Secure Custom Fields plugin. + */ + +( function () { + registerBlockBindingsSource( { + name: 'acf/field', + label: 'SCF Custom Fields', + getValues: function ( block ) { + console.log( block ); + }, + } ); +} )(); diff --git a/includes/Blocks/Bindings.php b/includes/Blocks/Bindings.php index 921558c3..2eda2f19 100644 --- a/includes/Blocks/Bindings.php +++ b/includes/Blocks/Bindings.php @@ -31,7 +31,6 @@ public function __construct() { * Hooked to acf/init, register our binding sources. */ public function register_binding_sources() { - if ( acf_get_setting( 'enable_block_bindings' ) ) { register_block_bindings_source( 'acf/field', array( @@ -39,7 +38,6 @@ public function register_binding_sources() { 'get_value_callback' => array( $this, 'get_value' ), ) ); - } } /** diff --git a/includes/acf-pattern-functions.php b/includes/acf-pattern-functions.php new file mode 100644 index 00000000..6e492e9a --- /dev/null +++ b/includes/acf-pattern-functions.php @@ -0,0 +1,10 @@ + Date: Fri, 23 May 2025 11:44:41 +0200 Subject: [PATCH 02/20] Experimenting, images still not working --- .phpcs.xml.dist | 1 + includes/Blocks/Bindings.php | 59 +++++ includes/acf-pattern-functions.php | 331 ++++++++++++++++++++++++++++- 3 files changed, 388 insertions(+), 3 deletions(-) diff --git a/.phpcs.xml.dist b/.phpcs.xml.dist index 404cccb3..bf15db56 100644 --- a/.phpcs.xml.dist +++ b/.phpcs.xml.dist @@ -6,6 +6,7 @@ /vendor/ /node_modules/ /lang/* + /includes/acf-pattern-functions.php diff --git a/includes/Blocks/Bindings.php b/includes/Blocks/Bindings.php index 2eda2f19..c8dd559f 100644 --- a/includes/Blocks/Bindings.php +++ b/includes/Blocks/Bindings.php @@ -38,6 +38,65 @@ public function register_binding_sources() { 'get_value_callback' => array( $this, 'get_value' ), ) ); + register_block_bindings_source( + 'scf/field', + array( + 'label' => _x( 'SCF Fields', 'The core SCF block binding source name for fields on the current page', 'secure-custom-fields' ), + 'uses_context' => array( 'postId', 'postType' ), + 'get_value_callback' => array( $this, 'scf_get_block_binding_value' ), + ) + ); + } + + /** + * Handle returning the block binding value for an ACF meta value. + * + * @since SCF 6.5 + * + * @param array $source_attrs An array of the source attributes requested. + * @param \WP_Block $block_instance The block instance. + * @param string $attribute_name The block's bound attribute name. + * @return string|null The block binding value or an empty string on failure. + */ + public function scf_get_block_binding_value( $source_attrs, $block_instance, $attribute_name ) { + $post_id = $block_instance->context['postId'] ?? get_the_ID(); + + // Ensure we're using the parent post ID if this is a revision + if ( $post_id && wp_is_post_revision( $post_id ) ) { + $post_id = wp_get_post_parent_id( $post_id ); + } + + $field_name = $source_attrs['field'] ?? ''; + + if ( ! $post_id || ! $field_name ) { + return ''; + } + + $value = get_field( $field_name, $post_id ); + // Handle different field types based on attribute + switch ( $attribute_name ) { + case 'content': + return is_array( $value ) ? ( $value['alt'] ?? '' ) : (string) $value; + case 'src': + case 'url': + if ( is_array( $value ) && isset( $value['url'] ) ) { + return $value['url']; + } + if ( is_numeric( $value ) ) { + return wp_get_attachment_url( $value ); + } + return (string) $value; + case 'alt': + if ( is_array( $value ) && isset( $value['alt'] ) ) { + return $value['alt']; + } + if ( is_numeric( $value ) ) { + return get_post_meta( $value, '_wp_attachment_image_alt', true ); + } + return ''; + default: + return is_string( $value ) ? $value : ''; + } } /** diff --git a/includes/acf-pattern-functions.php b/includes/acf-pattern-functions.php index 6e492e9a..473259c3 100644 --- a/includes/acf-pattern-functions.php +++ b/includes/acf-pattern-functions.php @@ -1,10 +1,335 @@ 'Title', + 'slug' => 'Slug', + 'categories' => 'Categories', + 'keywords' => 'Keywords', + 'description' => 'Description', + 'scf_fieldgroup' => 'SCF Fieldgroup', + 'conversion' => 'Conversion', // auto, bindings, php + ); + + $meta_data = get_file_data( $pattern_directory, $headers ); + + if ( empty( $meta_data['title'] ) || empty( $meta_data['slug'] ) ) { + return new WP_Error( 'invalid_pattern', 'Pattern missing required title or slug' ); + } + + // Determine conversion method + $conversion_method = ( array_key_exists( 'conversion', $meta_data ) ) ? $meta_data['conversion'] : 'auto'; + + $pattern_content = ''; + + switch ( $conversion_method ) { + case 'bindings': + // Pure binding approach - read static content + $pattern_content = scf_get_static_pattern_content( $pattern_directory ); + break; + + case 'auto': + default: + // Smart conversion: PHP to bindings + $pattern_content = scf_convert_php_to_bindings( $pattern_directory ); + break; + } + + if ( is_wp_error( $pattern_content ) ) { + return $pattern_content; + } + + // Process metadata + $categories = ! empty( $meta_data['categories'] ) ? + array_map( 'trim', explode( ',', $meta_data['categories'] ) ) : array( 'text' ); + $keywords = ! empty( $meta_data['keywords'] ) ? + array_map( 'trim', explode( ',', $meta_data['keywords'] ) ) : array(); + + // Register pattern + register_block_pattern( + $meta_data['slug'], + array( + 'title' => $meta_data['title'], + 'categories' => $categories, + 'keywords' => $keywords, + 'description' => array_key_exists( 'description', $meta_data ) ? $meta_data['description'] : __( 'SCF Pattern', 'secure-custom-fields' ), + 'content' => $pattern_content, + ) + ); + + return array( + 'slug' => $meta_data['slug'], + 'title' => $meta_data['title'], + 'status' => 'registered', + 'method' => $conversion_method, + ); +} + +/** + * Convert PHP SCF get_field('my_field') to bindings. + * + * @param string $pattern_file The path to the pattern file. + * @return string The converted HTML content. + */ +function scf_convert_php_to_bindings( $pattern_file ) { + // Read the file content + if ( ! file_exists( $pattern_file ) || ! is_readable( $pattern_file ) ) { + return new WP_Error( 'pattern_not_readable', 'Pattern file is not readable' ); + } + + $content = file_get_contents( $pattern_file ); + if ( false === $content ) { + return new WP_Error( 'pattern_not_readable', 'Failed to read pattern file' ); + } + + // Extract PHP section and HTML section + if ( ! preg_match( '/^<\?php.*?\?>(.*)/s', $content, $matches ) ) { + // No PHP section, treat as static content + return trim( $content ); + } + + $php_section = $matches[0]; + $html_section = $matches[1]; + + // Parse PHP section to find get_field() calls + $field_mappings = scf_extract_field_mappings( $php_section ); + + // Convert HTML section by replacing PHP echoes with bindings + $converted_html = scf_replace_php_echoes_with_bindings( $html_section, $field_mappings ); + + return trim( $converted_html ); +} + +/** + * Extract field mappings from PHP. + * + * @param string $php_content The PHP content to extract field mappings from. + * @return array The field mappings. + */ +function scf_extract_field_mappings( $php_content ) { + $mappings = array(); + + // Pattern to match: $variable = get_field('field_name') with optional post ID + preg_match_all( '/\$(\w+)\s*=\s*get_field\([\'"]([^\'"]+)[\'"](?:\s*,\s*[^)]+)?\)/', $php_content, $matches, PREG_SET_ORDER ); + + foreach ( $matches as $match ) { + $variable = $match[1]; + $field_name = $match[2]; + $mappings[ $variable ] = $field_name; + } + + // Also match get_sub_field patterns + preg_match_all( '/\$(\w+)\s*=\s*get_sub_field\([\'"]([^\'"]+)[\'"]\)/', $php_content, $sub_matches, PREG_SET_ORDER ); + + foreach ( $sub_matches as $match ) { + $variable = $match[1]; + $field_name = $match[2]; + $mappings[ $variable ] = $field_name; + } + + // Match get_the_terms patterns + preg_match_all( '/\$(\w+)\s*=\s*get_the_terms\([^,]+,\s*[\'"]([^\'"]+)[\'"]\)/', $php_content, $terms_matches, PREG_SET_ORDER ); + + foreach ( $terms_matches as $match ) { + $variable = $match[1]; + $taxonomy_name = $match[2]; + $mappings[ $variable ] = $taxonomy_name; + } + + return $mappings; +} + +/** + * Replace PHP echoes with block bindings. + * + * @param string $html_content The HTML content to replace PHP echoes with block bindings. + * @param array $field_mappings The field mappings. + * @return string The converted HTML content. + */ +function scf_replace_php_echoes_with_bindings( $html_content, $field_mappings ) { + // Process each individual block, including nested ones + $html_content = scf_process_blocks_recursively( $html_content, $field_mappings ); + + return $html_content; +} + +/** + * Process blocks recursively. + * + * @param string $content The content to process. + * @param array $field_mappings The field mappings. + * @return string The processed content. + */ +function scf_process_blocks_recursively( $content, $field_mappings ) { + // Pattern to match WordPress blocks (including nested ones) + return preg_replace_callback( + '/(.*?)/s', + function ( $matches ) use ( $field_mappings ) { + $block_name = $matches[1]; + $attributes_str = $matches[2] ?? '{}'; + $block_content = $matches[3]; + + // Parse existing attributes + $attributes_str = $attributes_str ? $attributes_str : '{}'; + $attributes = json_decode( $attributes_str, true ); + $attributes = $attributes ? $attributes : array(); + + // Process nested blocks first + $block_content = scf_process_blocks_recursively( $block_content, $field_mappings ); + + // Now process PHP echoes in this block's direct content + $block_modified = false; + $block_content = preg_replace_callback( + '/<\?php\s+echo\s+esc_html\(\s*(?:the_title\(\)|\$(\w+))\s*\)\s*;\s*\?>|<\?php\s+echo\s+esc_url\(\s*\$(\w+)\s*\)\s*;\s*\?>|<\?php\s+echo\s+\$(\w+)\s*;\s*\?>|<\?php\s+the_title\(\)\s*;\s*\?>/', + function ( $echo_matches ) use ( $field_mappings, &$attributes, $block_name, &$block_modified ) { + // Handle different types of PHP echoes + if ( ! empty( $echo_matches[0] ) && ( strpos( $echo_matches[0], 'the_title()' ) !== false ) ) { + // Handle the_title() - bind to post title + if ( 'core/image' === $block_name ) { + $attributes['metadata']['bindings']['alt'] = array( + 'source' => 'core/post-meta', + 'args' => array( 'key' => 'title' ), + ); + } else { + $binding_attributes = scf_get_binding_attribute_for_block( $block_name ); + $primary_attribute = $binding_attributes[0] ?? 'content'; + $attributes['metadata']['bindings'][ $primary_attribute ] = array( + 'source' => 'core/post-meta', + 'args' => array( 'key' => 'title' ), + ); + } + $block_modified = true; + return 'Post Title'; + } + + // Extract variable name from different echo patterns + $variable = ''; + for ( $i = 1; $i <= 4; $i++ ) { + if ( ! empty( $echo_matches[ $i ] ) ) { + $variable = $echo_matches[ $i ]; + break; + } + } + + if ( $variable && isset( $field_mappings[ $variable ] ) ) { + $field_name = $field_mappings[ $variable ]; + + // Handle special cases for image blocks + if ( 'core/image' === $block_name ) { + if ( strpos( $echo_matches[0], 'esc_url' ) !== false ) { + $attributes['metadata']['bindings']['url'] = array( + 'source' => 'scf/field', + 'args' => array( 'field' => $field_name ), + ); + $block_modified = true; + return '/api/placeholder/400/300'; + } elseif ( strpos( $echo_matches[0], 'esc_html' ) !== false ) { + // This is the image ID + $attributes['id'] = 0; // Set a placeholder ID + $block_modified = true; + return '0'; + } + } else { + // For non-image blocks, use the standard binding + $binding_attributes = scf_get_binding_attribute_for_block( $block_name ); + $primary_attribute = $binding_attributes[0] ?? 'content'; + $attributes['metadata']['bindings'][ $primary_attribute ] = array( + 'source' => 'scf/field', + 'args' => array( 'field' => $field_name ), + ); + $block_modified = true; + return scf_get_placeholder_for_field( $field_name, $primary_attribute ); + } + } + + // Return original if no mapping found + return $echo_matches[0]; + }, + $block_content + ); + + // For image blocks, ensure we have the proper structure + if ( 'core/image' === $block_name ) { + // If we have bindings but no proper image structure, fix it + if ( isset( $attributes['metadata']['bindings'] ) && ! strpos( $block_content, '{$block_content}"; + }, + $content + ); +} + +/** + * Get the supported binding attributes for a block. + * + * @param string $block_name The name of the block. + * @return array The supported binding attributes for the block. + */ +function scf_get_binding_attribute_for_block( $block_name ) { + $supported_block_attributes = array( + 'core/paragraph' => array( 'content' ), + 'core/heading' => array( 'content' ), + 'core/image' => array( 'id', 'url', 'title', 'alt' ), + 'core/button' => array( 'url', 'text', 'linkTarget', 'rel' ), + ); + + return $supported_block_attributes[ $block_name ] ?? array( 'content' ); +} + +/** + * Get a placeholder value for a field based on its binding attribute. + * + * @param string $field_name The name of the field. + * @param string $binding_attribute The binding attribute to get a placeholder for. + * @return string The placeholder value. + */ +function scf_get_placeholder_for_field( $field_name, $binding_attribute = 'content' ) { + // Handle specific binding attributes that need special values + if ( 'url' === $binding_attribute || 'src' === $binding_attribute ) { + return '/api/placeholder/400/300'; + } + + if ( 'alt' === $binding_attribute ) { + return 'Image description'; + } + + if ( 'id' === $binding_attribute ) { + return '0'; + } + + if ( 'linkTarget' === $binding_attribute ) { + return '_self'; + } + + if ( 'rel' === $binding_attribute ) { + return ''; + } + + // For all other attributes, return a simple generic placeholder + return sprintf( '[%s]', ucwords( str_replace( array( '_', '-' ), ' ', $field_name ) ) ); } From 81b614c00d20eb7b10683756ad37d9da610324b8 Mon Sep 17 00:00:00 2001 From: Carlos Bravo <37012961+cbravobernal@users.noreply.github.com> Date: Fri, 23 May 2025 13:34:09 +0200 Subject: [PATCH 03/20] Some cleaning, image still failing --- includes/acf-pattern-functions.php | 183 +++++++++++++++++++---------- 1 file changed, 124 insertions(+), 59 deletions(-) diff --git a/includes/acf-pattern-functions.php b/includes/acf-pattern-functions.php index 473259c3..0eb10cfb 100644 --- a/includes/acf-pattern-functions.php +++ b/includes/acf-pattern-functions.php @@ -143,15 +143,6 @@ function scf_extract_field_mappings( $php_content ) { $mappings[ $variable ] = $field_name; } - // Match get_the_terms patterns - preg_match_all( '/\$(\w+)\s*=\s*get_the_terms\([^,]+,\s*[\'"]([^\'"]+)[\'"]\)/', $php_content, $terms_matches, PREG_SET_ORDER ); - - foreach ( $terms_matches as $match ) { - $variable = $match[1]; - $taxonomy_name = $match[2]; - $mappings[ $variable ] = $taxonomy_name; - } - return $mappings; } @@ -196,31 +187,11 @@ function ( $matches ) use ( $field_mappings ) { // Now process PHP echoes in this block's direct content $block_modified = false; $block_content = preg_replace_callback( - '/<\?php\s+echo\s+esc_html\(\s*(?:the_title\(\)|\$(\w+))\s*\)\s*;\s*\?>|<\?php\s+echo\s+esc_url\(\s*\$(\w+)\s*\)\s*;\s*\?>|<\?php\s+echo\s+\$(\w+)\s*;\s*\?>|<\?php\s+the_title\(\)\s*;\s*\?>/', + '/<\?php\s+echo\s+(?:esc_html|esc_attr|esc_url|esc_js|esc_textarea)\(\s*\$(\w+)\s*\)\s*;\s*\?>|<\?php\s+echo\s+\$(\w+)\s*;\s*\?>/', function ( $echo_matches ) use ( $field_mappings, &$attributes, $block_name, &$block_modified ) { - // Handle different types of PHP echoes - if ( ! empty( $echo_matches[0] ) && ( strpos( $echo_matches[0], 'the_title()' ) !== false ) ) { - // Handle the_title() - bind to post title - if ( 'core/image' === $block_name ) { - $attributes['metadata']['bindings']['alt'] = array( - 'source' => 'core/post-meta', - 'args' => array( 'key' => 'title' ), - ); - } else { - $binding_attributes = scf_get_binding_attribute_for_block( $block_name ); - $primary_attribute = $binding_attributes[0] ?? 'content'; - $attributes['metadata']['bindings'][ $primary_attribute ] = array( - 'source' => 'core/post-meta', - 'args' => array( 'key' => 'title' ), - ); - } - $block_modified = true; - return 'Post Title'; - } - // Extract variable name from different echo patterns $variable = ''; - for ( $i = 1; $i <= 4; $i++ ) { + for ( $i = 1; $i <= 2; $i++ ) { if ( ! empty( $echo_matches[ $i ] ) ) { $variable = $echo_matches[ $i ]; break; @@ -229,32 +200,34 @@ function ( $echo_matches ) use ( $field_mappings, &$attributes, $block_name, &$b if ( $variable && isset( $field_mappings[ $variable ] ) ) { $field_name = $field_mappings[ $variable ]; + $escape_type = ''; + + // Determine the escape type + if ( preg_match( '/esc_(\w+)\(/', $echo_matches[0], $escape_match ) ) { + $escape_type = $escape_match[1]; + } - // Handle special cases for image blocks - if ( 'core/image' === $block_name ) { - if ( strpos( $echo_matches[0], 'esc_url' ) !== false ) { - $attributes['metadata']['bindings']['url'] = array( - 'source' => 'scf/field', + // Get block binding configuration + $binding_config = scf_get_block_binding_config( $block_name, $escape_type ); + + if ( $binding_config ) { + // Apply the binding configuration + foreach ( $binding_config['attributes'] as $attr_name => $attr_config ) { + $attributes['metadata']['bindings'][ $attr_name ] = array( + 'source' => $attr_config['source'], 'args' => array( 'field' => $field_name ), ); - $block_modified = true; - return '/api/placeholder/400/300'; - } elseif ( strpos( $echo_matches[0], 'esc_html' ) !== false ) { - // This is the image ID - $attributes['id'] = 0; // Set a placeholder ID - $block_modified = true; - return '0'; } - } else { - // For non-image blocks, use the standard binding - $binding_attributes = scf_get_binding_attribute_for_block( $block_name ); - $primary_attribute = $binding_attributes[0] ?? 'content'; - $attributes['metadata']['bindings'][ $primary_attribute ] = array( - 'source' => 'scf/field', - 'args' => array( 'field' => $field_name ), - ); + + // Set any additional attributes + if ( isset( $binding_config['additional_attributes'] ) ) { + foreach ( $binding_config['additional_attributes'] as $attr_name => $attr_value ) { + $attributes[ $attr_name ] = $attr_value; + } + } + $block_modified = true; - return scf_get_placeholder_for_field( $field_name, $primary_attribute ); + return $binding_config['placeholder'] ?? scf_get_placeholder_for_field( $field_name, $binding_config['primary_attribute'] ?? 'content' ); } } @@ -264,13 +237,8 @@ function ( $echo_matches ) use ( $field_mappings, &$attributes, $block_name, &$b $block_content ); - // For image blocks, ensure we have the proper structure - if ( 'core/image' === $block_name ) { - // If we have bindings but no proper image structure, fix it - if ( isset( $attributes['metadata']['bindings'] ) && ! strpos( $block_content, ' array( + 'url' => array( + 'attributes' => array( + 'url' => array( 'source' => 'scf/field' ), + ), + 'placeholder' => '/api/placeholder/400/300', + ), + 'alt' => array( + 'attributes' => array( + 'alt' => array( 'source' => 'scf/field' ), + ), + 'placeholder' => 'Image description', + ), + 'id' => array( + 'attributes' => array( + 'id' => array( 'source' => 'scf/field' ), + ), + 'additional_attributes' => array( + 'id' => 0, + ), + 'placeholder' => '0', + ), + ), + 'core/heading' => array( + 'content' => array( + 'attributes' => array( + 'content' => array( 'source' => 'scf/field' ), + ), + 'primary_attribute' => 'content', + ), + ), + 'core/paragraph' => array( + 'content' => array( + 'attributes' => array( + 'content' => array( 'source' => 'scf/field' ), + ), + 'primary_attribute' => 'content', + ), + ), + 'core/button' => array( + 'url' => array( + 'attributes' => array( + 'url' => array( 'source' => 'scf/field' ), + ), + 'primary_attribute' => 'url', + ), + 'text' => array( + 'attributes' => array( + 'text' => array( 'source' => 'scf/field' ), + ), + 'primary_attribute' => 'text', + ), + ), + ); + + // Default configuration for unsupported blocks + $default_config = array( + 'content' => array( + 'attributes' => array( + 'content' => array( 'source' => 'scf/field' ), + ), + 'primary_attribute' => 'content', + ), + ); + + // Get block-specific config or default + $block_config = $configs[ $block_name ] ?? $default_config; + + // Return specific escape type config or default content config + return $block_config[ $escape_type ] ?? $block_config['content'] ?? null; +} + +/** + * Ensure proper block structure based on bindings. + * + * @param string $block_name The name of the block. + * @param string $content The block content. + * @param array $attributes The block attributes. + * @return string The properly structured block content. + */ +function scf_ensure_block_structure( $block_name, $content, $attributes ) { + if ( 'core/image' === $block_name && isset( $attributes['metadata']['bindings'] ) && ! strpos( $content, 'Image description'; + } + + return $content; +} + /** * Get the supported binding attributes for a block. * From 5edb9b1497382805c9f1aa9f3382d8b3db8db1c6 Mon Sep 17 00:00:00 2001 From: Carlos Bravo <37012961+cbravobernal@users.noreply.github.com> Date: Fri, 23 May 2025 17:00:41 +0200 Subject: [PATCH 04/20] Back to something working again --- includes/acf-pattern-functions.php | 183 ++++++++++------------------- 1 file changed, 59 insertions(+), 124 deletions(-) diff --git a/includes/acf-pattern-functions.php b/includes/acf-pattern-functions.php index 0eb10cfb..473259c3 100644 --- a/includes/acf-pattern-functions.php +++ b/includes/acf-pattern-functions.php @@ -143,6 +143,15 @@ function scf_extract_field_mappings( $php_content ) { $mappings[ $variable ] = $field_name; } + // Match get_the_terms patterns + preg_match_all( '/\$(\w+)\s*=\s*get_the_terms\([^,]+,\s*[\'"]([^\'"]+)[\'"]\)/', $php_content, $terms_matches, PREG_SET_ORDER ); + + foreach ( $terms_matches as $match ) { + $variable = $match[1]; + $taxonomy_name = $match[2]; + $mappings[ $variable ] = $taxonomy_name; + } + return $mappings; } @@ -187,11 +196,31 @@ function ( $matches ) use ( $field_mappings ) { // Now process PHP echoes in this block's direct content $block_modified = false; $block_content = preg_replace_callback( - '/<\?php\s+echo\s+(?:esc_html|esc_attr|esc_url|esc_js|esc_textarea)\(\s*\$(\w+)\s*\)\s*;\s*\?>|<\?php\s+echo\s+\$(\w+)\s*;\s*\?>/', + '/<\?php\s+echo\s+esc_html\(\s*(?:the_title\(\)|\$(\w+))\s*\)\s*;\s*\?>|<\?php\s+echo\s+esc_url\(\s*\$(\w+)\s*\)\s*;\s*\?>|<\?php\s+echo\s+\$(\w+)\s*;\s*\?>|<\?php\s+the_title\(\)\s*;\s*\?>/', function ( $echo_matches ) use ( $field_mappings, &$attributes, $block_name, &$block_modified ) { + // Handle different types of PHP echoes + if ( ! empty( $echo_matches[0] ) && ( strpos( $echo_matches[0], 'the_title()' ) !== false ) ) { + // Handle the_title() - bind to post title + if ( 'core/image' === $block_name ) { + $attributes['metadata']['bindings']['alt'] = array( + 'source' => 'core/post-meta', + 'args' => array( 'key' => 'title' ), + ); + } else { + $binding_attributes = scf_get_binding_attribute_for_block( $block_name ); + $primary_attribute = $binding_attributes[0] ?? 'content'; + $attributes['metadata']['bindings'][ $primary_attribute ] = array( + 'source' => 'core/post-meta', + 'args' => array( 'key' => 'title' ), + ); + } + $block_modified = true; + return 'Post Title'; + } + // Extract variable name from different echo patterns $variable = ''; - for ( $i = 1; $i <= 2; $i++ ) { + for ( $i = 1; $i <= 4; $i++ ) { if ( ! empty( $echo_matches[ $i ] ) ) { $variable = $echo_matches[ $i ]; break; @@ -200,34 +229,32 @@ function ( $echo_matches ) use ( $field_mappings, &$attributes, $block_name, &$b if ( $variable && isset( $field_mappings[ $variable ] ) ) { $field_name = $field_mappings[ $variable ]; - $escape_type = ''; - - // Determine the escape type - if ( preg_match( '/esc_(\w+)\(/', $echo_matches[0], $escape_match ) ) { - $escape_type = $escape_match[1]; - } - // Get block binding configuration - $binding_config = scf_get_block_binding_config( $block_name, $escape_type ); - - if ( $binding_config ) { - // Apply the binding configuration - foreach ( $binding_config['attributes'] as $attr_name => $attr_config ) { - $attributes['metadata']['bindings'][ $attr_name ] = array( - 'source' => $attr_config['source'], + // Handle special cases for image blocks + if ( 'core/image' === $block_name ) { + if ( strpos( $echo_matches[0], 'esc_url' ) !== false ) { + $attributes['metadata']['bindings']['url'] = array( + 'source' => 'scf/field', 'args' => array( 'field' => $field_name ), ); + $block_modified = true; + return '/api/placeholder/400/300'; + } elseif ( strpos( $echo_matches[0], 'esc_html' ) !== false ) { + // This is the image ID + $attributes['id'] = 0; // Set a placeholder ID + $block_modified = true; + return '0'; } - - // Set any additional attributes - if ( isset( $binding_config['additional_attributes'] ) ) { - foreach ( $binding_config['additional_attributes'] as $attr_name => $attr_value ) { - $attributes[ $attr_name ] = $attr_value; - } - } - + } else { + // For non-image blocks, use the standard binding + $binding_attributes = scf_get_binding_attribute_for_block( $block_name ); + $primary_attribute = $binding_attributes[0] ?? 'content'; + $attributes['metadata']['bindings'][ $primary_attribute ] = array( + 'source' => 'scf/field', + 'args' => array( 'field' => $field_name ), + ); $block_modified = true; - return $binding_config['placeholder'] ?? scf_get_placeholder_for_field( $field_name, $binding_config['primary_attribute'] ?? 'content' ); + return scf_get_placeholder_for_field( $field_name, $primary_attribute ); } } @@ -237,8 +264,13 @@ function ( $echo_matches ) use ( $field_mappings, &$attributes, $block_name, &$b $block_content ); - // Ensure proper block structure based on bindings - $block_content = scf_ensure_block_structure( $block_name, $block_content, $attributes ); + // For image blocks, ensure we have the proper structure + if ( 'core/image' === $block_name ) { + // If we have bindings but no proper image structure, fix it + if ( isset( $attributes['metadata']['bindings'] ) && ! strpos( $block_content, ' array( - 'url' => array( - 'attributes' => array( - 'url' => array( 'source' => 'scf/field' ), - ), - 'placeholder' => '/api/placeholder/400/300', - ), - 'alt' => array( - 'attributes' => array( - 'alt' => array( 'source' => 'scf/field' ), - ), - 'placeholder' => 'Image description', - ), - 'id' => array( - 'attributes' => array( - 'id' => array( 'source' => 'scf/field' ), - ), - 'additional_attributes' => array( - 'id' => 0, - ), - 'placeholder' => '0', - ), - ), - 'core/heading' => array( - 'content' => array( - 'attributes' => array( - 'content' => array( 'source' => 'scf/field' ), - ), - 'primary_attribute' => 'content', - ), - ), - 'core/paragraph' => array( - 'content' => array( - 'attributes' => array( - 'content' => array( 'source' => 'scf/field' ), - ), - 'primary_attribute' => 'content', - ), - ), - 'core/button' => array( - 'url' => array( - 'attributes' => array( - 'url' => array( 'source' => 'scf/field' ), - ), - 'primary_attribute' => 'url', - ), - 'text' => array( - 'attributes' => array( - 'text' => array( 'source' => 'scf/field' ), - ), - 'primary_attribute' => 'text', - ), - ), - ); - - // Default configuration for unsupported blocks - $default_config = array( - 'content' => array( - 'attributes' => array( - 'content' => array( 'source' => 'scf/field' ), - ), - 'primary_attribute' => 'content', - ), - ); - - // Get block-specific config or default - $block_config = $configs[ $block_name ] ?? $default_config; - - // Return specific escape type config or default content config - return $block_config[ $escape_type ] ?? $block_config['content'] ?? null; -} - -/** - * Ensure proper block structure based on bindings. - * - * @param string $block_name The name of the block. - * @param string $content The block content. - * @param array $attributes The block attributes. - * @return string The properly structured block content. - */ -function scf_ensure_block_structure( $block_name, $content, $attributes ) { - if ( 'core/image' === $block_name && isset( $attributes['metadata']['bindings'] ) && ! strpos( $content, 'Image description'; - } - - return $content; -} - /** * Get the supported binding attributes for a block. * From 37d3297cba0d5aea7b234d3a84a30c671d152dfd Mon Sep 17 00:00:00 2001 From: Carlos Bravo <37012961+cbravobernal@users.noreply.github.com> Date: Fri, 23 May 2025 17:05:42 +0200 Subject: [PATCH 05/20] Image fixed --- includes/acf-pattern-functions.php | 24 ++---------------------- 1 file changed, 2 insertions(+), 22 deletions(-) diff --git a/includes/acf-pattern-functions.php b/includes/acf-pattern-functions.php index 473259c3..dd376813 100644 --- a/includes/acf-pattern-functions.php +++ b/includes/acf-pattern-functions.php @@ -198,26 +198,6 @@ function ( $matches ) use ( $field_mappings ) { $block_content = preg_replace_callback( '/<\?php\s+echo\s+esc_html\(\s*(?:the_title\(\)|\$(\w+))\s*\)\s*;\s*\?>|<\?php\s+echo\s+esc_url\(\s*\$(\w+)\s*\)\s*;\s*\?>|<\?php\s+echo\s+\$(\w+)\s*;\s*\?>|<\?php\s+the_title\(\)\s*;\s*\?>/', function ( $echo_matches ) use ( $field_mappings, &$attributes, $block_name, &$block_modified ) { - // Handle different types of PHP echoes - if ( ! empty( $echo_matches[0] ) && ( strpos( $echo_matches[0], 'the_title()' ) !== false ) ) { - // Handle the_title() - bind to post title - if ( 'core/image' === $block_name ) { - $attributes['metadata']['bindings']['alt'] = array( - 'source' => 'core/post-meta', - 'args' => array( 'key' => 'title' ), - ); - } else { - $binding_attributes = scf_get_binding_attribute_for_block( $block_name ); - $primary_attribute = $binding_attributes[0] ?? 'content'; - $attributes['metadata']['bindings'][ $primary_attribute ] = array( - 'source' => 'core/post-meta', - 'args' => array( 'key' => 'title' ), - ); - } - $block_modified = true; - return 'Post Title'; - } - // Extract variable name from different echo patterns $variable = ''; for ( $i = 1; $i <= 4; $i++ ) { @@ -231,7 +211,7 @@ function ( $echo_matches ) use ( $field_mappings, &$attributes, $block_name, &$b $field_name = $field_mappings[ $variable ]; // Handle special cases for image blocks - if ( 'core/image' === $block_name ) { + if ( 'image' === $block_name ) { if ( strpos( $echo_matches[0], 'esc_url' ) !== false ) { $attributes['metadata']['bindings']['url'] = array( 'source' => 'scf/field', @@ -265,7 +245,7 @@ function ( $echo_matches ) use ( $field_mappings, &$attributes, $block_name, &$b ); // For image blocks, ensure we have the proper structure - if ( 'core/image' === $block_name ) { + if ( 'image' === $block_name ) { // If we have bindings but no proper image structure, fix it if ( isset( $attributes['metadata']['bindings'] ) && ! strpos( $block_content, ' Date: Wed, 28 May 2025 14:10:16 +0200 Subject: [PATCH 06/20] Simplify pattern creation with just PHP functions --- includes/acf-pattern-functions.php | 421 ++++++++++++----------------- 1 file changed, 172 insertions(+), 249 deletions(-) diff --git a/includes/acf-pattern-functions.php b/includes/acf-pattern-functions.php index dd376813..d5417327 100644 --- a/includes/acf-pattern-functions.php +++ b/includes/acf-pattern-functions.php @@ -24,7 +24,6 @@ function scf_register_smart_pattern( $pattern_directory ) { 'keywords' => 'Keywords', 'description' => 'Description', 'scf_fieldgroup' => 'SCF Fieldgroup', - 'conversion' => 'Conversion', // auto, bindings, php ); $meta_data = get_file_data( $pattern_directory, $headers ); @@ -33,23 +32,8 @@ function scf_register_smart_pattern( $pattern_directory ) { return new WP_Error( 'invalid_pattern', 'Pattern missing required title or slug' ); } - // Determine conversion method - $conversion_method = ( array_key_exists( 'conversion', $meta_data ) ) ? $meta_data['conversion'] : 'auto'; - - $pattern_content = ''; - - switch ( $conversion_method ) { - case 'bindings': - // Pure binding approach - read static content - $pattern_content = scf_get_static_pattern_content( $pattern_directory ); - break; - - case 'auto': - default: - // Smart conversion: PHP to bindings - $pattern_content = scf_convert_php_to_bindings( $pattern_directory ); - break; - } + // Use the new pattern loading method that doesn't require output buffering + $pattern_content = scf_load_pattern_from_file( $pattern_directory ); if ( is_wp_error( $pattern_content ) ) { return $pattern_content; @@ -72,244 +56,183 @@ function scf_register_smart_pattern( $pattern_directory ) { 'content' => $pattern_content, ) ); - - return array( - 'slug' => $meta_data['slug'], - 'title' => $meta_data['title'], - 'status' => 'registered', - 'method' => $conversion_method, - ); } -/** - * Convert PHP SCF get_field('my_field') to bindings. - * - * @param string $pattern_file The path to the pattern file. - * @return string The converted HTML content. - */ -function scf_convert_php_to_bindings( $pattern_file ) { - // Read the file content - if ( ! file_exists( $pattern_file ) || ! is_readable( $pattern_file ) ) { - return new WP_Error( 'pattern_not_readable', 'Pattern file is not readable' ); - } - - $content = file_get_contents( $pattern_file ); - if ( false === $content ) { - return new WP_Error( 'pattern_not_readable', 'Failed to read pattern file' ); +function create_block_with_binding( string $tag, string $source, array $bindings_args = array(), string $inner_content = '' ) { + // If tag is specified, map it to the appropriate block type + $block = 'core/paragraph'; // Default block type + $wrapper_tag = 'p'; // Default HTML wrapper tag + $attributes = array(); // Block attributes + + if ($tag !== null) { + switch ($tag) { + case 'p': + $block = 'core/paragraph'; + $wrapper_tag = 'p'; + break; + case 'h1': + $block = 'core/heading'; + $wrapper_tag = 'h1'; + $attributes['level'] = 1; + break; + case 'h2': + case 'h': // Support legacy 'h' tag as h2 + $block = 'core/heading'; + $wrapper_tag = 'h2'; + $attributes['level'] = 2; + break; + case 'h3': + $block = 'core/heading'; + $wrapper_tag = 'h3'; + $attributes['level'] = 3; + break; + case 'h4': + $block = 'core/heading'; + $wrapper_tag = 'h4'; + $attributes['level'] = 4; + break; + case 'h5': + $block = 'core/heading'; + $wrapper_tag = 'h5'; + $attributes['level'] = 5; + break; + case 'h6': + $block = 'core/heading'; + $wrapper_tag = 'h6'; + $attributes['level'] = 6; + break; + case 'figure': + $block = 'core/image'; + $wrapper_tag = 'figure'; + break; + case 'img': + $block = 'core/image'; + $wrapper_tag = 'figure'; + break; + case 'button': + $block = 'core/button'; + $wrapper_tag = 'div'; + break; + case 'div': + $block = 'core/group'; + $wrapper_tag = 'div'; + break; + default: + $block = 'core/paragraph'; + $wrapper_tag = 'p'; + break; + } + } + + + // Create inner content with the correct HTML structure + $class_attr = ''; + if (strpos($block, 'heading') !== false) { + $class_attr = ' class="wp-block-heading"'; + } + if (strpos($block, 'image') !== false) { + $class_attr = ' class="wp-block-image"'; } - - // Extract PHP section and HTML section - if ( ! preg_match( '/^<\?php.*?\?>(.*)/s', $content, $matches ) ) { - // No PHP section, treat as static content - return trim( $content ); - } - - $php_section = $matches[0]; - $html_section = $matches[1]; - - // Parse PHP section to find get_field() calls - $field_mappings = scf_extract_field_mappings( $php_section ); - - // Convert HTML section by replacing PHP echoes with bindings - $converted_html = scf_replace_php_echoes_with_bindings( $html_section, $field_mappings ); - - return trim( $converted_html ); -} - -/** - * Extract field mappings from PHP. - * - * @param string $php_content The PHP content to extract field mappings from. - * @return array The field mappings. - */ -function scf_extract_field_mappings( $php_content ) { - $mappings = array(); - - // Pattern to match: $variable = get_field('field_name') with optional post ID - preg_match_all( '/\$(\w+)\s*=\s*get_field\([\'"]([^\'"]+)[\'"](?:\s*,\s*[^)]+)?\)/', $php_content, $matches, PREG_SET_ORDER ); - - foreach ( $matches as $match ) { - $variable = $match[1]; - $field_name = $match[2]; - $mappings[ $variable ] = $field_name; - } - - // Also match get_sub_field patterns - preg_match_all( '/\$(\w+)\s*=\s*get_sub_field\([\'"]([^\'"]+)[\'"]\)/', $php_content, $sub_matches, PREG_SET_ORDER ); - - foreach ( $sub_matches as $match ) { - $variable = $match[1]; - $field_name = $match[2]; - $mappings[ $variable ] = $field_name; - } - - // Match get_the_terms patterns - preg_match_all( '/\$(\w+)\s*=\s*get_the_terms\([^,]+,\s*[\'"]([^\'"]+)[\'"]\)/', $php_content, $terms_matches, PREG_SET_ORDER ); - - foreach ( $terms_matches as $match ) { - $variable = $match[1]; - $taxonomy_name = $match[2]; - $mappings[ $variable ] = $taxonomy_name; - } - - return $mappings; -} - -/** - * Replace PHP echoes with block bindings. - * - * @param string $html_content The HTML content to replace PHP echoes with block bindings. - * @param array $field_mappings The field mappings. - * @return string The converted HTML content. - */ -function scf_replace_php_echoes_with_bindings( $html_content, $field_mappings ) { - // Process each individual block, including nested ones - $html_content = scf_process_blocks_recursively( $html_content, $field_mappings ); - - return $html_content; + + // Generate content with proper HTML structure + if (empty(trim($inner_content))) { + if ($tag === 'img' || $tag === 'figure') { + $inner_content = sprintf('<%1$s%3$s>%2$s', $wrapper_tag, esc_attr(''), $class_attr); + } else { + $inner_content = sprintf('<%1$s%3$s>%2$s', $wrapper_tag, esc_attr(''), $class_attr); + } + + } else { + // Check if we need to add proper HTML structure + if (!preg_match('/^\s*<' . preg_quote($wrapper_tag, '/') . '[\s>]/i', $inner_content)) { + // Add class for headings if needed + $inner_content = sprintf('<%1$s%3$s>%2$s', $wrapper_tag, $inner_content, $class_attr); + } + } + + // Build block attributes JSON + $attr_json = ''; + if (!empty($bindings_args)) { + // Initialize metadata bindings array + $attributes['metadata'] = array( + 'bindings' => array() + ); + + // Process each binding argument + foreach ((array)$bindings_args as $binding) { + // Check if this is a properly formatted binding + if (isset($binding['attribute']) && isset($binding['field'])) { + $attributes['metadata']['bindings'][$binding['attribute']] = array( + 'source' => $source, + 'args' => array( + 'field' => $binding['field'] + ) + ); + } + } + + $attr_json = wp_json_encode($attributes); + } + + // Format according to WordPress block structure + $content = sprintf( + ' +%s +', + esc_attr($block), + $attr_json, + $inner_content, + esc_attr($block) + ); + + return $content; } /** - * Process blocks recursively. + * Load pattern content from a file by reading and processing it directly. * - * @param string $content The content to process. - * @param array $field_mappings The field mappings. - * @return string The processed content. - */ -function scf_process_blocks_recursively( $content, $field_mappings ) { - // Pattern to match WordPress blocks (including nested ones) - return preg_replace_callback( - '/(.*?)/s', - function ( $matches ) use ( $field_mappings ) { - $block_name = $matches[1]; - $attributes_str = $matches[2] ?? '{}'; - $block_content = $matches[3]; - - // Parse existing attributes - $attributes_str = $attributes_str ? $attributes_str : '{}'; - $attributes = json_decode( $attributes_str, true ); - $attributes = $attributes ? $attributes : array(); - - // Process nested blocks first - $block_content = scf_process_blocks_recursively( $block_content, $field_mappings ); - - // Now process PHP echoes in this block's direct content - $block_modified = false; - $block_content = preg_replace_callback( - '/<\?php\s+echo\s+esc_html\(\s*(?:the_title\(\)|\$(\w+))\s*\)\s*;\s*\?>|<\?php\s+echo\s+esc_url\(\s*\$(\w+)\s*\)\s*;\s*\?>|<\?php\s+echo\s+\$(\w+)\s*;\s*\?>|<\?php\s+the_title\(\)\s*;\s*\?>/', - function ( $echo_matches ) use ( $field_mappings, &$attributes, $block_name, &$block_modified ) { - // Extract variable name from different echo patterns - $variable = ''; - for ( $i = 1; $i <= 4; $i++ ) { - if ( ! empty( $echo_matches[ $i ] ) ) { - $variable = $echo_matches[ $i ]; - break; - } - } - - if ( $variable && isset( $field_mappings[ $variable ] ) ) { - $field_name = $field_mappings[ $variable ]; - - // Handle special cases for image blocks - if ( 'image' === $block_name ) { - if ( strpos( $echo_matches[0], 'esc_url' ) !== false ) { - $attributes['metadata']['bindings']['url'] = array( - 'source' => 'scf/field', - 'args' => array( 'field' => $field_name ), - ); - $block_modified = true; - return '/api/placeholder/400/300'; - } elseif ( strpos( $echo_matches[0], 'esc_html' ) !== false ) { - // This is the image ID - $attributes['id'] = 0; // Set a placeholder ID - $block_modified = true; - return '0'; - } - } else { - // For non-image blocks, use the standard binding - $binding_attributes = scf_get_binding_attribute_for_block( $block_name ); - $primary_attribute = $binding_attributes[0] ?? 'content'; - $attributes['metadata']['bindings'][ $primary_attribute ] = array( - 'source' => 'scf/field', - 'args' => array( 'field' => $field_name ), - ); - $block_modified = true; - return scf_get_placeholder_for_field( $field_name, $primary_attribute ); - } - } - - // Return original if no mapping found - return $echo_matches[0]; - }, - $block_content - ); - - // For image blocks, ensure we have the proper structure - if ( 'image' === $block_name ) { - // If we have bindings but no proper image structure, fix it - if ( isset( $attributes['metadata']['bindings'] ) && ! strpos( $block_content, '{$block_content}"; - }, - $content - ); -} - -/** - * Get the supported binding attributes for a block. + * This is the recommended method to use instead of scf_parse_pattern_file() + * as it doesn't rely on output buffering, which can cause issues. * - * @param string $block_name The name of the block. - * @return array The supported binding attributes for the block. + * @since SCF 6.5.1 + * @param string $pattern_file The pattern file path. + * @return string|WP_Error The pattern content or WP_Error on failure. */ -function scf_get_binding_attribute_for_block( $block_name ) { - $supported_block_attributes = array( - 'core/paragraph' => array( 'content' ), - 'core/heading' => array( 'content' ), - 'core/image' => array( 'id', 'url', 'title', 'alt' ), - 'core/button' => array( 'url', 'text', 'linkTarget', 'rel' ), - ); - - return $supported_block_attributes[ $block_name ] ?? array( 'content' ); -} - -/** - * Get a placeholder value for a field based on its binding attribute. - * - * @param string $field_name The name of the field. - * @param string $binding_attribute The binding attribute to get a placeholder for. - * @return string The placeholder value. - */ -function scf_get_placeholder_for_field( $field_name, $binding_attribute = 'content' ) { - // Handle specific binding attributes that need special values - if ( 'url' === $binding_attribute || 'src' === $binding_attribute ) { - return '/api/placeholder/400/300'; - } - - if ( 'alt' === $binding_attribute ) { - return 'Image description'; - } - - if ( 'id' === $binding_attribute ) { - return '0'; - } - - if ( 'linkTarget' === $binding_attribute ) { - return '_self'; - } - - if ( 'rel' === $binding_attribute ) { - return ''; - } - - // For all other attributes, return a simple generic placeholder - return sprintf( '[%s]', ucwords( str_replace( array( '_', '-' ), ' ', $field_name ) ) ); +function scf_load_pattern_from_file( $pattern_file ) { + if ( ! file_exists( $pattern_file ) || ! is_readable( $pattern_file ) ) { + return new WP_Error( 'pattern_not_found', 'Pattern file not found or not readable' ); + } + + // For PHP files, execute directly without reading the entire file first + if ( pathinfo( $pattern_file, PATHINFO_EXTENSION ) === 'php' ) { + try { + // Create a closure that mimics the include environment but returns the content + $sandbox = function( $file_path ) { + ob_start(); + $result = include $file_path; + $output = ob_get_clean(); + + // If the file returns a string directly (recommended pattern), + // use that instead of captured output + if ( is_string( $result ) ) { + return $result; + } + + // Otherwise return the captured output + return $output; + }; + + return $sandbox( $pattern_file ); + } catch ( Exception $e ) { + return new WP_Error( 'pattern_execution_error', $e->getMessage() ); + } + } + + // For non-PHP files (like HTML), only now do we read the file + $file_content = file_get_contents( $pattern_file ); + if ( false === $file_content ) { + return new WP_Error( 'pattern_read_error', 'Unable to read pattern file contents' ); + } + + return $file_content; } From 2b15a95eada464c3815ed49e7a443ce67e4edee9 Mon Sep 17 00:00:00 2001 From: Carlos Bravo <37012961+cbravobernal@users.noreply.github.com> Date: Wed, 28 May 2025 14:51:47 +0200 Subject: [PATCH 07/20] Enable as an experiment --- includes/acf-pattern-functions.php | 4 +- includes/admin/beta-features.php | 10 ++--- .../class-scf-beta-feature-code-patterns.php | 37 +++++++++++++++++++ secure-custom-fields.php | 5 ++- 4 files changed, 48 insertions(+), 8 deletions(-) create mode 100644 includes/admin/beta-features/class-scf-beta-feature-code-patterns.php diff --git a/includes/acf-pattern-functions.php b/includes/acf-pattern-functions.php index d5417327..64d879d7 100644 --- a/includes/acf-pattern-functions.php +++ b/includes/acf-pattern-functions.php @@ -12,7 +12,7 @@ * @param string $pattern_directory The directory containing the pattern file. * @return array|WP_Error The pattern registration result or a WP_Error if the pattern is invalid. */ -function scf_register_smart_pattern( $pattern_directory ) { +function experimental_scf_register_pattern( $pattern_directory ) { if ( ! file_exists( $pattern_directory ) || ! is_readable( $pattern_directory ) ) { return new WP_Error( 'pattern_not_found', 'Pattern file not found' ); } @@ -58,7 +58,7 @@ function scf_register_smart_pattern( $pattern_directory ) { ); } -function create_block_with_binding( string $tag, string $source, array $bindings_args = array(), string $inner_content = '' ) { +function experimental_create_block_with_binding( string $tag, string $source, array $bindings_args = array(), string $inner_content = '' ) { // If tag is specified, map it to the appropriate block type $block = 'core/paragraph'; // Default block type $wrapper_tag = 'p'; // Default HTML wrapper tag diff --git a/includes/admin/beta-features.php b/includes/admin/beta-features.php index 1051fd58..5f32bf49 100644 --- a/includes/admin/beta-features.php +++ b/includes/admin/beta-features.php @@ -35,8 +35,7 @@ class SCF_Admin_Beta_Features { * @return void */ public function __construct() { - // Temporarily disabled - will be enabled when beta feature is ready - // add_action( 'admin_menu', array( $this, 'admin_menu' ), 20 ); + add_action( 'admin_menu', array( $this, 'admin_menu' ), 20 ); } /** @@ -88,7 +87,8 @@ public function get_beta_features() { public function localize_beta_features() { $beta_features = array(); foreach ( $this->get_beta_features() as $name => $beta_feature ) { - $beta_features[ $name ] = $beta_feature->is_enabled(); + $is_enabled = $beta_feature->is_enabled(); + $beta_features[ $name ] = $is_enabled; } acf_localize_data( @@ -155,7 +155,7 @@ public function admin_body_class( $classes ) { */ private function include_beta_features() { acf_include( 'includes/admin/beta-features/class-scf-beta-feature.php' ); - acf_include( 'includes/admin/beta-features/class-scf-beta-feature-editor-sidebar.php' ); + acf_include( 'includes/admin/beta-features/class-scf-beta-feature-code-patterns.php' ); add_action( 'scf/include_admin_beta_features', array( $this, 'register_beta_features' ) ); @@ -170,7 +170,7 @@ private function include_beta_features() { * @return void */ public function register_beta_features() { - scf_register_admin_beta_feature( 'SCF_Admin_Beta_Feature_Editor_Sidebar' ); + scf_register_admin_beta_feature( 'SCF_Admin_Beta_Feature_Code_Patterns' ); } /** diff --git a/includes/admin/beta-features/class-scf-beta-feature-code-patterns.php b/includes/admin/beta-features/class-scf-beta-feature-code-patterns.php new file mode 100644 index 00000000..a0bb8016 --- /dev/null +++ b/includes/admin/beta-features/class-scf-beta-feature-code-patterns.php @@ -0,0 +1,37 @@ +name = 'code_patterns'; + $this->title = __( 'Add SCF Code Patterns', 'secure-custom-fields' ); + $this->description = __( 'Provides an API to register code patterns.', 'secure-custom-fields' ); + } + } +endif; diff --git a/secure-custom-fields.php b/secure-custom-fields.php index 5e6bacbf..2ae913ab 100644 --- a/secure-custom-fields.php +++ b/secure-custom-fields.php @@ -169,7 +169,9 @@ public function initialize() { acf_include( 'includes/acf-field-group-functions.php' ); acf_include( 'includes/acf-form-functions.php' ); acf_include( 'includes/acf-meta-functions.php' ); - acf_include( 'includes/acf-pattern-functions.php' ); + if ( get_option( 'scf_beta_feature_code_patterns_enabled' ) ) { + acf_include( 'includes/acf-pattern-functions.php' ); + } acf_include( 'includes/acf-post-functions.php' ); acf_include( 'includes/acf-user-functions.php' ); acf_include( 'includes/acf-value-functions.php' ); @@ -880,6 +882,7 @@ function scf_plugin_uninstall() { // List of known beta features. $beta_features = array( 'editor_sidebar', + 'code_patterns', ); foreach ( $beta_features as $beta_feature ) { From 64cde78a8d091f5b100b419ea6c73f3da46329f6 Mon Sep 17 00:00:00 2001 From: Carlos Bravo <37012961+cbravobernal@users.noreply.github.com> Date: Wed, 28 May 2025 17:07:06 +0200 Subject: [PATCH 08/20] Fix images --- includes/Blocks/Bindings.php | 11 +++++++++-- includes/acf-pattern-functions.php | 5 +++-- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/includes/Blocks/Bindings.php b/includes/Blocks/Bindings.php index c8dd559f..aad5ca35 100644 --- a/includes/Blocks/Bindings.php +++ b/includes/Blocks/Bindings.php @@ -39,7 +39,7 @@ public function register_binding_sources() { ) ); register_block_bindings_source( - 'scf/field', + 'scf/experimental-field', array( 'label' => _x( 'SCF Fields', 'The core SCF block binding source name for fields on the current page', 'secure-custom-fields' ), 'uses_context' => array( 'postId', 'postType' ), @@ -77,7 +77,6 @@ public function scf_get_block_binding_value( $source_attrs, $block_instance, $at switch ( $attribute_name ) { case 'content': return is_array( $value ) ? ( $value['alt'] ?? '' ) : (string) $value; - case 'src': case 'url': if ( is_array( $value ) && isset( $value['url'] ) ) { return $value['url']; @@ -94,6 +93,14 @@ public function scf_get_block_binding_value( $source_attrs, $block_instance, $at return get_post_meta( $value, '_wp_attachment_image_alt', true ); } return ''; + case 'id': + if ( is_array( $value ) && isset( $value['id'] ) ) { + return (string) $value['id']; + } + if ( is_numeric( $value ) ) { + return (string) $value; + } + return ''; default: return is_string( $value ) ? $value : ''; } diff --git a/includes/acf-pattern-functions.php b/includes/acf-pattern-functions.php index 64d879d7..6ac10629 100644 --- a/includes/acf-pattern-functions.php +++ b/includes/acf-pattern-functions.php @@ -58,7 +58,7 @@ function experimental_scf_register_pattern( $pattern_directory ) { ); } -function experimental_create_block_with_binding( string $tag, string $source, array $bindings_args = array(), string $inner_content = '' ) { +function experimental_create_block_with_binding( string $tag, array $bindings_args = array(), string $inner_content = '' ) { // If tag is specified, map it to the appropriate block type $block = 'core/paragraph'; // Default block type $wrapper_tag = 'p'; // Default HTML wrapper tag @@ -163,7 +163,8 @@ function experimental_create_block_with_binding( string $tag, string $source, ar // Check if this is a properly formatted binding if (isset($binding['attribute']) && isset($binding['field'])) { $attributes['metadata']['bindings'][$binding['attribute']] = array( - 'source' => $source, + // TODO: We can pass the source as a variable so it will work with any binding source. + 'source' => 'scf/experimental-field', 'args' => array( 'field' => $binding['field'] ) From d6bd7d83b384900738f4cf91068be3f20075562e Mon Sep 17 00:00:00 2001 From: Carlos Bravo <37012961+cbravobernal@users.noreply.github.com> Date: Wed, 28 May 2025 17:50:05 +0200 Subject: [PATCH 09/20] Group it if not before --- includes/acf-pattern-functions.php | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/includes/acf-pattern-functions.php b/includes/acf-pattern-functions.php index 6ac10629..750010fb 100644 --- a/includes/acf-pattern-functions.php +++ b/includes/acf-pattern-functions.php @@ -163,7 +163,7 @@ function experimental_create_block_with_binding( string $tag, array $bindings_ar // Check if this is a properly formatted binding if (isset($binding['attribute']) && isset($binding['field'])) { $attributes['metadata']['bindings'][$binding['attribute']] = array( - // TODO: We can pass the source as a variable so it will work with any binding source. + // TODO: We can pass the source as a variable so it will work with any source. 'source' => 'scf/experimental-field', 'args' => array( 'field' => $binding['field'] @@ -223,17 +223,25 @@ function scf_load_pattern_from_file( $pattern_file ) { return $output; }; - return $sandbox( $pattern_file ); + $pattern_content = $sandbox( $pattern_file ); } catch ( Exception $e ) { return new WP_Error( 'pattern_execution_error', $e->getMessage() ); } + } else { + // For non-PHP files (like HTML), only now do we read the file + $pattern_content = file_get_contents( $pattern_file ); + if ( false === $pattern_content ) { + return new WP_Error( 'pattern_read_error', 'Unable to read pattern file contents' ); + } } - - // For non-PHP files (like HTML), only now do we read the file - $file_content = file_get_contents( $pattern_file ); - if ( false === $file_content ) { - return new WP_Error( 'pattern_read_error', 'Unable to read pattern file contents' ); + + // Wrap the pattern content in a group block if it's not already a group block + if (!preg_match('/^\n
%s
\n", + $pattern_content + ); } - return $file_content; + return $pattern_content; } From d7cef4af903f14e58436f15119d9ffea963688f4 Mon Sep 17 00:00:00 2001 From: Carlos Bravo <37012961+cbravobernal@users.noreply.github.com> Date: Wed, 28 May 2025 19:07:03 +0200 Subject: [PATCH 10/20] Now is working on the editor --- assets/src/js/acf.js | 1 - assets/src/js/bindings/custom-sources.js | 85 ++++++++++++++++++++++++ assets/src/js/bindings/index.js | 1 + assets/src/js/custom-sources.js | 22 ------ includes/assets.php | 12 ++++ webpack.config.js | 4 +- 6 files changed, 101 insertions(+), 24 deletions(-) create mode 100644 assets/src/js/bindings/custom-sources.js create mode 100644 assets/src/js/bindings/index.js delete mode 100644 assets/src/js/custom-sources.js diff --git a/assets/src/js/acf.js b/assets/src/js/acf.js index 215fa7c9..7ca4222e 100644 --- a/assets/src/js/acf.js +++ b/assets/src/js/acf.js @@ -6,4 +6,3 @@ import './_acf-modal.js'; import './_acf-panel.js'; import './_acf-notice.js'; import './_acf-tooltip.js'; -import './custom-sources.js'; diff --git a/assets/src/js/bindings/custom-sources.js b/assets/src/js/bindings/custom-sources.js new file mode 100644 index 00000000..86e14045 --- /dev/null +++ b/assets/src/js/bindings/custom-sources.js @@ -0,0 +1,85 @@ +/** + * WordPress dependencies. + */ +import { __ } from '@wordpress/i18n'; +import { registerBlockBindingsSource } from '@wordpress/blocks'; +import { store as coreDataStore } from '@wordpress/core-data'; + +/** + * Register SCF Custom Fields block binding source. + * + * This allows blocks to bind to custom fields managed by + * the Secure Custom Fields plugin. + */ +console.log( 'Registering SCF Custom Fields block binding source...' ); +registerBlockBindingsSource( { + name: 'scf/experimental-field', + label: 'SCF Custom Fields', + getValues: function ( { context, clientId, bindings, select } ) { + const { getEditedEntityRecord, getMedia } = select( coreDataStore ); + let fields; + if ( context?.postType && context?.postId ) { + fields = getEditedEntityRecord( + 'postType', + context?.postType, + context?.postId + ); + } + + const result = {}; + + // Process each binding attribute (id, url, alt, title, content) + Object.keys( bindings ).forEach( ( attribute ) => { + const fieldName = bindings[ attribute ]?.args?.field; + if ( fieldName && fields?.acf ) { + const fieldValue = fields.acf[ fieldName ]; + + // Handle image fields which return objects with specific properties + if ( typeof fieldValue === 'object' && fieldValue !== null ) { + // Directly map the attribute to the corresponding property in the image object + if ( attribute in fieldValue ) { + result[ attribute ] = fieldValue[ attribute ]; + } else if ( attribute === 'content' && fieldValue.url ) { + // If content is requested for an image field, use url + result[ attribute ] = fieldValue.url; + } else { + result[ attribute ] = ''; + } + } + // Handle when field value is just an image ID number + else if ( typeof fieldValue === 'number' ) { + // Use core data store to get full image details + const imageObj = getMedia( fieldValue ); + + if ( imageObj ) { + if ( attribute === 'id' ) { + result[ attribute ] = imageObj.id; + } else if ( + attribute === 'url' || + attribute === 'content' + ) { + result[ attribute ] = imageObj.source_url; + } else if ( attribute === 'alt' ) { + result[ attribute ] = imageObj.alt_text || ''; + } else if ( attribute === 'title' ) { + result[ attribute ] = + imageObj.title?.rendered || ''; + } else { + result[ attribute ] = ''; + } + } else { + // Image data not yet available + result[ attribute ] = ''; + } + } else { + // For simple field values + result[ attribute ] = fieldValue || ''; + } + } else { + result[ attribute ] = ''; + } + } ); + + return result; + }, +} ); diff --git a/assets/src/js/bindings/index.js b/assets/src/js/bindings/index.js new file mode 100644 index 00000000..4102bb53 --- /dev/null +++ b/assets/src/js/bindings/index.js @@ -0,0 +1 @@ +import './custom-sources.js'; diff --git a/assets/src/js/custom-sources.js b/assets/src/js/custom-sources.js deleted file mode 100644 index 703a06f5..00000000 --- a/assets/src/js/custom-sources.js +++ /dev/null @@ -1,22 +0,0 @@ -/** - * WordPress dependencies. - */ -import { __ } from '@wordpress/i18n'; -import { registerBlockBindingsSource } from '@wordpress/blocks'; - -/** - * Register SCF Custom Fields block binding source. - * - * This allows blocks to bind to custom fields managed by - * the Secure Custom Fields plugin. - */ - -( function () { - registerBlockBindingsSource( { - name: 'acf/field', - label: 'SCF Custom Fields', - getValues: function ( block ) { - console.log( block ); - }, - } ); -} )(); diff --git a/includes/assets.php b/includes/assets.php index 5b166adb..ecddcdab 100644 --- a/includes/assets.php +++ b/includes/assets.php @@ -189,6 +189,14 @@ public function register_scripts() { 'version' => $version, 'in_footer' => true, ), + 'scf-bindings' => array( + 'handle' => 'scf-bindings', + 'src' => acf_get_url( sprintf( $js_path_patterns['base'], 'scf-bindings' ) ), + 'asset_file' => acf_get_path( sprintf( $asset_path_patterns['base'], 'scf-bindings' ) ), + 'version' => $version, + 'deps' => array(), + 'in_footer' => true, + ), ); // Define style registrations. @@ -539,6 +547,10 @@ public function enqueue_scripts() { do_action( 'acf/input/admin_enqueue_scripts' ); } + if ( get_option( 'scf_beta_feature_code_patterns_enabled' ) ) { + wp_enqueue_script( 'scf-bindings' ); + } + /** * Fires during "admin_enqueue_scripts" when ACF scripts are enqueued. * diff --git a/webpack.config.js b/webpack.config.js index 699b7fe2..08f4ff7d 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -15,8 +15,10 @@ const commonConfig = { 'js/acf-input': './assets/src/js/acf-input.js', 'js/acf-internal-post-type': './assets/src/js/acf-internal-post-type.js', + 'js/scf-bindings': './assets/src/js/bindings/index.js', 'js/commands/scf-admin': './assets/src/js/commands/admin-commands.js', - 'js/commands/scf-custom-post-types': './assets/src/js/commands/custom-post-type-commands.js', + 'js/commands/scf-custom-post-types': + './assets/src/js/commands/custom-post-type-commands.js', 'js/acf': './assets/src/js/acf.js', 'js/pro/acf-pro-blocks': './assets/src/js/pro/acf-pro-blocks.js', 'js/pro/acf-pro-field-group': From 6e60b6b901b082e470838559a5f7a9491146ed83 Mon Sep 17 00:00:00 2001 From: Carlos Bravo <37012961+cbravobernal@users.noreply.github.com> Date: Thu, 29 May 2025 17:47:43 +0200 Subject: [PATCH 11/20] Update and edit values --- assets/src/js/bindings/custom-sources.js | 72 +++++++++++++++++++++++- 1 file changed, 71 insertions(+), 1 deletion(-) diff --git a/assets/src/js/bindings/custom-sources.js b/assets/src/js/bindings/custom-sources.js index 86e14045..679054e3 100644 --- a/assets/src/js/bindings/custom-sources.js +++ b/assets/src/js/bindings/custom-sources.js @@ -4,6 +4,9 @@ import { __ } from '@wordpress/i18n'; import { registerBlockBindingsSource } from '@wordpress/blocks'; import { store as coreDataStore } from '@wordpress/core-data'; +import { store as editorStore } from '@wordpress/editor'; +import { addAction, removeAction } from '@wordpress/hooks'; +import { select, dispatch } from '@wordpress/data'; /** * Register SCF Custom Fields block binding source. @@ -11,7 +14,6 @@ import { store as coreDataStore } from '@wordpress/core-data'; * This allows blocks to bind to custom fields managed by * the Secure Custom Fields plugin. */ -console.log( 'Registering SCF Custom Fields block binding source...' ); registerBlockBindingsSource( { name: 'scf/experimental-field', label: 'SCF Custom Fields', @@ -82,4 +84,72 @@ registerBlockBindingsSource( { return result; }, + setValues: function ( { context, bindings, dispatch, select } ) { + const { getEditedEntityRecord } = select( coreDataStore ); + + // Make sure we have bindings and context + if ( ! bindings || ! context?.postType || ! context?.postId ) { + return; + } + + const postType = context.postType; + const postId = context.postId; + + // Get the current post data to preserve existing values + const currentPost = getEditedEntityRecord( + 'postType', + postType, + postId + ); + const currentAcfData = currentPost?.acf || {}; + + // Prepare the fields object to update + const fieldsToUpdate = {}; + + // Process each binding + Object.keys( bindings ).forEach( ( attribute ) => { + const binding = bindings[ attribute ]; + const fieldName = binding?.args?.field; + const newValue = binding?.newValue; + + // Skip if no field name or new value + if ( ! fieldName || newValue === undefined ) { + return; + } + + // For image fields, we need special handling since multiple attributes + // might refer to the same field + if ( ! fieldsToUpdate[ fieldName ] ) { + // First attribute for this field + fieldsToUpdate[ fieldName ] = newValue; + } else if ( + attribute === 'url' && + typeof fieldsToUpdate[ fieldName ] === 'object' + ) { + // For image fields, update the url property if it's already an object + fieldsToUpdate[ fieldName ] = { + ...fieldsToUpdate[ fieldName ], + url: newValue, + }; + } else if ( attribute === 'id' && typeof newValue === 'number' ) { + // If it's an image ID, store just the ID + fieldsToUpdate[ fieldName ] = newValue; + } + dispatch( coreDataStore ).editEntityRecord( + 'postType', + postType, + postId, + { + acf: { + ...currentAcfData, + ...fieldsToUpdate, + }, + meta: { _acf_changed: 1 }, + } + ); + } ); + }, + canUserEditValue: function () { + return true; + }, } ); From 4668dda466552ecc1e5f0e9d92905b7f51a1d260 Mon Sep 17 00:00:00 2001 From: Carlos Bravo <37012961+cbravobernal@users.noreply.github.com> Date: Thu, 29 May 2025 18:43:52 +0200 Subject: [PATCH 12/20] Now saves correctly --- assets/src/js/bindings/custom-sources.js | 4 ++-- includes/forms/form-gutenberg.php | 6 ++++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/assets/src/js/bindings/custom-sources.js b/assets/src/js/bindings/custom-sources.js index 679054e3..8ce9fa76 100644 --- a/assets/src/js/bindings/custom-sources.js +++ b/assets/src/js/bindings/custom-sources.js @@ -84,7 +84,7 @@ registerBlockBindingsSource( { return result; }, - setValues: function ( { context, bindings, dispatch, select } ) { + setValues: async function ( { context, bindings, dispatch, select } ) { const { getEditedEntityRecord } = select( coreDataStore ); // Make sure we have bindings and context @@ -107,7 +107,7 @@ registerBlockBindingsSource( { const fieldsToUpdate = {}; // Process each binding - Object.keys( bindings ).forEach( ( attribute ) => { + Object.keys( bindings ).forEach( async ( attribute ) => { const binding = bindings[ attribute ]; const fieldName = binding?.args?.field; const newValue = binding?.newValue; diff --git a/includes/forms/form-gutenberg.php b/includes/forms/form-gutenberg.php index 5acb081e..7336904b 100644 --- a/includes/forms/form-gutenberg.php +++ b/includes/forms/form-gutenberg.php @@ -69,7 +69,9 @@ function enqueue_block_editor_assets() { function add_meta_boxes() { // Remove 'edit_form_after_title' action. - remove_action( 'edit_form_after_title', array( acf_get_instance( 'ACF_Form_Post' ), 'edit_form_after_title' ) ); + if ( ! get_option( 'scf_beta_feature_code_patterns_enabled' ) ) { + remove_action( 'edit_form_after_title', array( acf_get_instance( 'ACF_Form_Post' ), 'edit_form_after_title' ) ); + } } /** @@ -173,7 +175,7 @@ function modify_user_option_meta_box_order( $locations ) { function acf_validate_save_post() { // Check if current request came from Gutenberg. - if ( isset( $_GET['meta-box-loader'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Verified elsewhere. + if ( isset( $_GET['meta-box-loader'] ) && get_option ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Verified elsewhere. acf_reset_validation_errors(); } } From 99fcf7d854b576b332c0fec3dbd200adef7599be Mon Sep 17 00:00:00 2001 From: Carlos Bravo <37012961+cbravobernal@users.noreply.github.com> Date: Tue, 3 Jun 2025 12:45:00 +0200 Subject: [PATCH 13/20] Not ready, update metaboxes --- assets/src/js/bindings/custom-sources.js | 7 +++++++ includes/acf-pattern-functions.php | 4 ++-- includes/forms/form-gutenberg.php | 2 +- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/assets/src/js/bindings/custom-sources.js b/assets/src/js/bindings/custom-sources.js index 8ce9fa76..246fc4a3 100644 --- a/assets/src/js/bindings/custom-sources.js +++ b/assets/src/js/bindings/custom-sources.js @@ -119,6 +119,12 @@ registerBlockBindingsSource( { // For image fields, we need special handling since multiple attributes // might refer to the same field + + // TODO: Remove metabox, use just the sidebar. + document + .querySelector( `[data-name="${ fieldName }"] input` ) + .setAttribute( 'value', newValue ); + if ( ! fieldsToUpdate[ fieldName ] ) { // First attribute for this field fieldsToUpdate[ fieldName ] = newValue; @@ -135,6 +141,7 @@ registerBlockBindingsSource( { // If it's an image ID, store just the ID fieldsToUpdate[ fieldName ] = newValue; } + dispatch( coreDataStore ).editEntityRecord( 'postType', postType, diff --git a/includes/acf-pattern-functions.php b/includes/acf-pattern-functions.php index 750010fb..34e50b20 100644 --- a/includes/acf-pattern-functions.php +++ b/includes/acf-pattern-functions.php @@ -10,7 +10,7 @@ * * @since SCF 6.5.0 * @param string $pattern_directory The directory containing the pattern file. - * @return array|WP_Error The pattern registration result or a WP_Error if the pattern is invalid. + * @return bool|WP_Error The pattern registration result or a WP_Error if the pattern is invalid. */ function experimental_scf_register_pattern( $pattern_directory ) { if ( ! file_exists( $pattern_directory ) || ! is_readable( $pattern_directory ) ) { @@ -46,7 +46,7 @@ function experimental_scf_register_pattern( $pattern_directory ) { array_map( 'trim', explode( ',', $meta_data['keywords'] ) ) : array(); // Register pattern - register_block_pattern( + return register_block_pattern( $meta_data['slug'], array( 'title' => $meta_data['title'], diff --git a/includes/forms/form-gutenberg.php b/includes/forms/form-gutenberg.php index 7336904b..6749927b 100644 --- a/includes/forms/form-gutenberg.php +++ b/includes/forms/form-gutenberg.php @@ -175,7 +175,7 @@ function modify_user_option_meta_box_order( $locations ) { function acf_validate_save_post() { // Check if current request came from Gutenberg. - if ( isset( $_GET['meta-box-loader'] ) && get_option ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Verified elsewhere. + if ( isset( $_GET['meta-box-loader'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Verified elsewhere. acf_reset_validation_errors(); } } From 41b2bed644c7e101d09c6a152c7f3730fc4703bd Mon Sep 17 00:00:00 2001 From: Carlos Bravo <37012961+cbravobernal@users.noreply.github.com> Date: Tue, 3 Jun 2025 14:03:44 +0200 Subject: [PATCH 14/20] A slighlty better way to handle the post-meta update --- assets/src/js/bindings/custom-sources.js | 5 ----- includes/acf-form-functions.php | 5 +++++ 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/assets/src/js/bindings/custom-sources.js b/assets/src/js/bindings/custom-sources.js index 246fc4a3..52d226db 100644 --- a/assets/src/js/bindings/custom-sources.js +++ b/assets/src/js/bindings/custom-sources.js @@ -120,11 +120,6 @@ registerBlockBindingsSource( { // For image fields, we need special handling since multiple attributes // might refer to the same field - // TODO: Remove metabox, use just the sidebar. - document - .querySelector( `[data-name="${ fieldName }"] input` ) - .setAttribute( 'value', newValue ); - if ( ! fieldsToUpdate[ fieldName ] ) { // First attribute for this field fieldsToUpdate[ fieldName ] = newValue; diff --git a/includes/acf-form-functions.php b/includes/acf-form-functions.php index d025937d..cf3559aa 100644 --- a/includes/acf-form-functions.php +++ b/includes/acf-form-functions.php @@ -129,6 +129,11 @@ function acf_save_post( $post_id = 0, $values = null ) { return false; } + // Prevent auto-save, as we do it before in custom-sources.js. + if ( get_option( 'scf_beta_feature_code_patterns_enabled' ) ) { + return false; + } + // Set form data (useful in various filters/actions). acf_set_form_data( 'post_id', $post_id ); From bc491b97f086b3cd7623912b00c78c495d6f721e Mon Sep 17 00:00:00 2001 From: Carlos Bravo <37012961+cbravobernal@users.noreply.github.com> Date: Wed, 4 Jun 2025 14:51:58 +0200 Subject: [PATCH 15/20] Clean custom-sources file --- assets/src/js/bindings/custom-sources.js | 163 +++++++++-------------- 1 file changed, 61 insertions(+), 102 deletions(-) diff --git a/assets/src/js/bindings/custom-sources.js b/assets/src/js/bindings/custom-sources.js index 52d226db..9e27de22 100644 --- a/assets/src/js/bindings/custom-sources.js +++ b/assets/src/js/bindings/custom-sources.js @@ -4,154 +4,113 @@ import { __ } from '@wordpress/i18n'; import { registerBlockBindingsSource } from '@wordpress/blocks'; import { store as coreDataStore } from '@wordpress/core-data'; -import { store as editorStore } from '@wordpress/editor'; -import { addAction, removeAction } from '@wordpress/hooks'; -import { select, dispatch } from '@wordpress/data'; /** - * Register SCF Custom Fields block binding source. + * Get the value of a specific field from the ACF fields. * - * This allows blocks to bind to custom fields managed by - * the Secure Custom Fields plugin. + * @param {Object} fields The ACF fields object. + * @param {string} fieldName The name of the field to retrieve. + * @returns {string} The value of the specified field, or undefined if not found. */ +const getFieldValue = ( fields, fieldName ) => fields?.acf?.[ fieldName ]; + +const resolveImageAttribute = ( imageObj, attribute ) => { + if ( ! imageObj ) return ''; + switch ( attribute ) { + case 'id': + return imageObj.id; + case 'url': + case 'content': + return imageObj.source_url; + case 'alt': + return imageObj.alt_text || ''; + case 'title': + return imageObj.title?.rendered || ''; + default: + return ''; + } +}; + registerBlockBindingsSource( { name: 'scf/experimental-field', label: 'SCF Custom Fields', - getValues: function ( { context, clientId, bindings, select } ) { + getValues( { context, bindings, select } ) { const { getEditedEntityRecord, getMedia } = select( coreDataStore ); - let fields; - if ( context?.postType && context?.postId ) { - fields = getEditedEntityRecord( - 'postType', - context?.postType, - context?.postId - ); - } - + let fields = + context?.postType && context?.postId + ? getEditedEntityRecord( + 'postType', + context.postType, + context.postId + ) + : undefined; const result = {}; - // Process each binding attribute (id, url, alt, title, content) - Object.keys( bindings ).forEach( ( attribute ) => { - const fieldName = bindings[ attribute ]?.args?.field; - if ( fieldName && fields?.acf ) { - const fieldValue = fields.acf[ fieldName ]; + Object.entries( bindings ).forEach( + ( [ attribute, { args } = {} ] ) => { + const fieldName = args?.field; + const fieldValue = getFieldValue( fields, fieldName ); - // Handle image fields which return objects with specific properties if ( typeof fieldValue === 'object' && fieldValue !== null ) { - // Directly map the attribute to the corresponding property in the image object - if ( attribute in fieldValue ) { - result[ attribute ] = fieldValue[ attribute ]; - } else if ( attribute === 'content' && fieldValue.url ) { - // If content is requested for an image field, use url - result[ attribute ] = fieldValue.url; - } else { - result[ attribute ] = ''; - } - } - // Handle when field value is just an image ID number - else if ( typeof fieldValue === 'number' ) { - // Use core data store to get full image details + result[ attribute ] = + ( fieldValue[ attribute ] ?? + ( attribute === 'content' && fieldValue.url ) ) || + ''; + } else if ( typeof fieldValue === 'number' ) { const imageObj = getMedia( fieldValue ); - - if ( imageObj ) { - if ( attribute === 'id' ) { - result[ attribute ] = imageObj.id; - } else if ( - attribute === 'url' || - attribute === 'content' - ) { - result[ attribute ] = imageObj.source_url; - } else if ( attribute === 'alt' ) { - result[ attribute ] = imageObj.alt_text || ''; - } else if ( attribute === 'title' ) { - result[ attribute ] = - imageObj.title?.rendered || ''; - } else { - result[ attribute ] = ''; - } - } else { - // Image data not yet available - result[ attribute ] = ''; - } + result[ attribute ] = resolveImageAttribute( + imageObj, + attribute + ); } else { - // For simple field values result[ attribute ] = fieldValue || ''; } - } else { - result[ attribute ] = ''; } - } ); - + ); return result; }, - setValues: async function ( { context, bindings, dispatch, select } ) { + async setValues( { context, bindings, dispatch, select } ) { const { getEditedEntityRecord } = select( coreDataStore ); - - // Make sure we have bindings and context - if ( ! bindings || ! context?.postType || ! context?.postId ) { - return; - } + if ( ! bindings || ! context?.postType || ! context?.postId ) return; const postType = context.postType; const postId = context.postId; - - // Get the current post data to preserve existing values const currentPost = getEditedEntityRecord( 'postType', postType, postId ); const currentAcfData = currentPost?.acf || {}; - - // Prepare the fields object to update const fieldsToUpdate = {}; - // Process each binding - Object.keys( bindings ).forEach( async ( attribute ) => { - const binding = bindings[ attribute ]; + for ( const [ attribute, binding ] of Object.entries( bindings ) ) { const fieldName = binding?.args?.field; const newValue = binding?.newValue; - - // Skip if no field name or new value - if ( ! fieldName || newValue === undefined ) { - return; - } - - // For image fields, we need special handling since multiple attributes - // might refer to the same field - + if ( ! fieldName || newValue === undefined ) continue; if ( ! fieldsToUpdate[ fieldName ] ) { - // First attribute for this field fieldsToUpdate[ fieldName ] = newValue; } else if ( attribute === 'url' && typeof fieldsToUpdate[ fieldName ] === 'object' ) { - // For image fields, update the url property if it's already an object fieldsToUpdate[ fieldName ] = { ...fieldsToUpdate[ fieldName ], url: newValue, }; } else if ( attribute === 'id' && typeof newValue === 'number' ) { - // If it's an image ID, store just the ID fieldsToUpdate[ fieldName ] = newValue; } + } - dispatch( coreDataStore ).editEntityRecord( - 'postType', - postType, - postId, - { - acf: { - ...currentAcfData, - ...fieldsToUpdate, - }, - meta: { _acf_changed: 1 }, - } - ); - } ); - }, - canUserEditValue: function () { - return true; + dispatch( coreDataStore ).editEntityRecord( + 'postType', + postType, + postId, + { + acf: { ...currentAcfData, ...fieldsToUpdate }, + meta: { _acf_changed: 1 }, + } + ); }, + canUserEditValue: () => true, } ); From 136e5644aac5697518007498b8229e04b768d893 Mon Sep 17 00:00:00 2001 From: Carlos Bravo <37012961+cbravobernal@users.noreply.github.com> Date: Wed, 4 Jun 2025 15:02:11 +0200 Subject: [PATCH 16/20] Add usercanedit depending on role --- assets/src/js/bindings/custom-sources.js | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/assets/src/js/bindings/custom-sources.js b/assets/src/js/bindings/custom-sources.js index 9e27de22..a0b9aff1 100644 --- a/assets/src/js/bindings/custom-sources.js +++ b/assets/src/js/bindings/custom-sources.js @@ -112,5 +112,27 @@ registerBlockBindingsSource( { } ); }, - canUserEditValue: () => true, + canUserEditValue( { select, context, args } ) { + // Lock editing in query loop. + if ( context?.query || context?.queryId ) { + return false; + } + + // Lock editing when `postType` is not defined. + if ( ! context?.postType ) { + return false; + } + + // Check that the user has the capability to edit post meta. + const canUserEdit = select( coreDataStore ).canUser( 'update', { + kind: 'postType', + name: context?.postType, + id: context?.postId, + } ); + if ( ! canUserEdit ) { + return false; + } + + return true; + }, } ); From 77b88cc5700c9d3631e715cd84ad21092ceda0fa Mon Sep 17 00:00:00 2001 From: Carlos Bravo <37012961+cbravobernal@users.noreply.github.com> Date: Thu, 5 Jun 2025 14:28:45 +0200 Subject: [PATCH 17/20] Now works with empty posts --- assets/src/js/bindings/custom-sources.js | 68 +++++++++++++++++++++--- 1 file changed, 61 insertions(+), 7 deletions(-) diff --git a/assets/src/js/bindings/custom-sources.js b/assets/src/js/bindings/custom-sources.js index a0b9aff1..16d1c1cb 100644 --- a/assets/src/js/bindings/custom-sources.js +++ b/assets/src/js/bindings/custom-sources.js @@ -73,12 +73,10 @@ registerBlockBindingsSource( { const { getEditedEntityRecord } = select( coreDataStore ); if ( ! bindings || ! context?.postType || ! context?.postId ) return; - const postType = context.postType; - const postId = context.postId; const currentPost = getEditedEntityRecord( 'postType', - postType, - postId + context.postType, + context.postId ); const currentAcfData = currentPost?.acf || {}; const fieldsToUpdate = {}; @@ -102,12 +100,68 @@ registerBlockBindingsSource( { } } + // Format ACF data properly before saving + const formattedAcfData = { ...currentAcfData }; + + // Process all ACF fields to ensure proper types + const allAcfFields = { ...formattedAcfData, ...fieldsToUpdate }; + const processedAcfData = {}; + + for ( const [ key, value ] of Object.entries( allAcfFields ) ) { + // Handle specific field types requiring proper type conversion + if ( value === '' ) { + // Convert empty strings to appropriate types based on field name + if ( + key === 'number' || + key.includes( '_number' ) || + /number$/.test( key ) + ) { + // Number fields should be null when empty + processedAcfData[ key ] = null; + } else if ( key.includes( 'range' ) || key === 'range_type' ) { + // Range fields should be null when empty + processedAcfData[ key ] = null; + } else if ( key.includes( '_date' ) ) { + // Date fields should be null when empty + processedAcfData[ key ] = null; + } else if ( key.includes( 'email' ) || key === 'email_type' ) { + // Handle email fields + processedAcfData[ key ] = null; + } else if ( key.includes( 'url' ) || key === 'url_type' ) { + // Handle URL fields + processedAcfData[ key ] = null; + } else { + // Other fields can remain as empty strings + processedAcfData[ key ] = value; + } + } else if ( value === 0 || value ) { + // Non-empty values - ensure numbers are actually numbers + if ( + ( key === 'number' || + key.includes( '_number' ) || + /number$/.test( key ) ) && + value !== null + ) { + // Convert string numbers to actual numbers if needed + const numValue = parseFloat( value ); + processedAcfData[ key ] = isNaN( numValue ) + ? null + : numValue; + } else { + processedAcfData[ key ] = value; + } + } else { + // null, undefined, etc. + processedAcfData[ key ] = value; + } + } + dispatch( coreDataStore ).editEntityRecord( 'postType', - postType, - postId, + context.postType, + context.postId, { - acf: { ...currentAcfData, ...fieldsToUpdate }, + acf: processedAcfData, meta: { _acf_changed: 1 }, } ); From d8a99f5ce0cf2d046f54e0455c8f6b5ca4f1e3ef Mon Sep 17 00:00:00 2001 From: Carlos Bravo <37012961+cbravobernal@users.noreply.github.com> Date: Fri, 6 Jun 2025 10:54:54 +0200 Subject: [PATCH 18/20] Fix working with type number --- assets/src/js/bindings/custom-sources.js | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/assets/src/js/bindings/custom-sources.js b/assets/src/js/bindings/custom-sources.js index 16d1c1cb..60dc7664 100644 --- a/assets/src/js/bindings/custom-sources.js +++ b/assets/src/js/bindings/custom-sources.js @@ -49,24 +49,30 @@ registerBlockBindingsSource( { Object.entries( bindings ).forEach( ( [ attribute, { args } = {} ] ) => { const fieldName = args?.field; - const fieldValue = getFieldValue( fields, fieldName ); + console.log( 'fieldName', fieldName ); + const fieldValue = getFieldValue( fields, fieldName ); if ( typeof fieldValue === 'object' && fieldValue !== null ) { result[ attribute ] = ( fieldValue[ attribute ] ?? ( attribute === 'content' && fieldValue.url ) ) || ''; } else if ( typeof fieldValue === 'number' ) { - const imageObj = getMedia( fieldValue ); - result[ attribute ] = resolveImageAttribute( - imageObj, - attribute - ); + if ( attribute === 'content' ) { + result[ attribute ] = fieldValue.toString() || ''; + } else { + const imageObj = getMedia( fieldValue ); + result[ attribute ] = resolveImageAttribute( + imageObj, + attribute + ); + } } else { result[ attribute ] = fieldValue || ''; } } ); + console.log( 'result', result ); return result; }, async setValues( { context, bindings, dispatch, select } ) { From 845bf93f99bbd7705b76ce978cf65f91c529b7b6 Mon Sep 17 00:00:00 2001 From: Carlos Bravo <37012961+cbravobernal@users.noreply.github.com> Date: Fri, 6 Jun 2025 11:54:54 +0200 Subject: [PATCH 19/20] Remove clogs --- assets/src/js/bindings/custom-sources.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/assets/src/js/bindings/custom-sources.js b/assets/src/js/bindings/custom-sources.js index 60dc7664..c9062d1b 100644 --- a/assets/src/js/bindings/custom-sources.js +++ b/assets/src/js/bindings/custom-sources.js @@ -49,7 +49,6 @@ registerBlockBindingsSource( { Object.entries( bindings ).forEach( ( [ attribute, { args } = {} ] ) => { const fieldName = args?.field; - console.log( 'fieldName', fieldName ); const fieldValue = getFieldValue( fields, fieldName ); if ( typeof fieldValue === 'object' && fieldValue !== null ) { @@ -72,7 +71,6 @@ registerBlockBindingsSource( { } } ); - console.log( 'result', result ); return result; }, async setValues( { context, bindings, dispatch, select } ) { From fe360dbe8114810e2a423a803f56fa8f53a0864c Mon Sep 17 00:00:00 2001 From: Carlos Bravo <37012961+cbravobernal@users.noreply.github.com> Date: Fri, 6 Jun 2025 12:10:47 +0200 Subject: [PATCH 20/20] Remove not needed code --- assets/src/js/bindings/custom-sources.js | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/assets/src/js/bindings/custom-sources.js b/assets/src/js/bindings/custom-sources.js index c9062d1b..c91c4164 100644 --- a/assets/src/js/bindings/custom-sources.js +++ b/assets/src/js/bindings/custom-sources.js @@ -104,11 +104,7 @@ registerBlockBindingsSource( { } } - // Format ACF data properly before saving - const formattedAcfData = { ...currentAcfData }; - - // Process all ACF fields to ensure proper types - const allAcfFields = { ...formattedAcfData, ...fieldsToUpdate }; + const allAcfFields = { ...currentAcfData, ...fieldsToUpdate }; const processedAcfData = {}; for ( const [ key, value ] of Object.entries( allAcfFields ) ) {