From 1e72ae1c5f8cbdaf5e3aa33b6d0ba969c2b26969 Mon Sep 17 00:00:00 2001 From: girishpanchal30 Date: Wed, 15 Apr 2026 18:35:06 +0530 Subject: [PATCH 1/3] feat: add ACF repeater field support --- inc/plugins/class-dynamic-content.php | 110 ++++++- inc/server/class-dynamic-content-server.php | 34 +- .../inc/plugins/class-dynamic-content.php | 294 ++++++++++++++++-- src/pro/components/acf-image-select/index.js | 91 ++++-- src/pro/plugins/data/index.js | 36 ++- src/pro/plugins/dynamic-content/link-edit.js | 51 ++- src/pro/plugins/dynamic-content/value-edit.js | 53 +++- 7 files changed, 554 insertions(+), 115 deletions(-) diff --git a/inc/plugins/class-dynamic-content.php b/inc/plugins/class-dynamic-content.php index 80354989d..af2f121a9 100644 --- a/inc/plugins/class-dynamic-content.php +++ b/inc/plugins/class-dynamic-content.php @@ -60,7 +60,26 @@ public function apply_dynamic_content( $content ) { } $position = strlen( $position ); - $content = substr_replace( $content, $replacement, $position, strlen( $string_to_replace ) ); + if ( is_array( $replacement ) ) { + $re = '/#otterDynamicLink\/?.[^"]*/'; + $matches = array(); + $num = preg_match_all( $re, $content, $matches, PREG_SET_ORDER, 0 ); + + if ( isset( $matches[0] ) ) { + $link = $this->apply_link_button( $matches[0] ); + } + $_content = array_fill( 0, count( $replacement ), $content ); + $content = ''; + foreach ( $replacement as $key => $value ) { + $updated_content = substr_replace( $_content[ $key ], $value, $position, strlen( $string_to_replace ) ); + if ( isset( $link ) && is_array( $link ) && isset( $link[ $key ] ) ) { + $updated_content = str_replace( $matches[0], $link[ $key ], $updated_content ); + } + $content .= $updated_content; + } + } else { + $content = substr_replace( $content, $replacement, $position, strlen( $string_to_replace ) ); + } } return $content; @@ -133,17 +152,34 @@ public function apply_magic_tags( $data ) { * Filter post content for dynamic link. * * @param string $content Post content. + * @param int|null $key Optional key for multiple dynamic links in the same content. * * @return string */ - public function apply_dynamic_link( $content ) { + public function apply_dynamic_link( $content, $key = null ) { if ( false === strpos( $content, '[^"\'<>]+)["\']|data-target=["\'](?P[^"\'<>]+)["\']|data-meta-key=["\'](?P[^"\'<>]+)["\']|data-context=["\'](?P[^"\'<>]+)["\']|[a-zA-Z-]+=["\'][^"\'<>]+["\']))*\s*>(?[^ $].*?)<\s*\/\s*o-dynamic-link>/'; - return preg_replace_callback( $re, array( $this, 'apply_link' ), $content ); + $matches = array(); + $num = preg_match_all( $re, $content, $matches, PREG_SET_ORDER, 0 ); + if ( isset( $num ) && 0 === $num ) { + return $content; + } + + $link = $this->apply_link( $matches[0], $key ); + + if ( is_array( $link ) ) { + $_content = array_fill( 0, count( $link ), $content ); + $content = ''; + foreach ( $link as $key => $value ) { + $content .= preg_replace( $re, $value, $_content[ $key ] ); + } + return $content; + } + return preg_replace( $re, $link, $content, 1 ); } /** @@ -178,9 +214,24 @@ public function apply_dynamic_images( $content ) { $rest_url = get_rest_url( null, 'otter/v1' ); $rest_url = preg_replace( '/([^A-Za-z0-9\s_-])/', '\\\\$1', $rest_url ); - $re = '/' . $rest_url . '\/dynamic\/?.[^"]*/'; + $re = '/' . $rest_url . '\/dynamic\/?.[^"]*/'; + $matches = array(); + $num = preg_match_all( $re, $content, $matches, PREG_SET_ORDER, 0 ); + if ( isset( $num ) && 0 === $num ) { + return $content; + } - return preg_replace_callback( $re, array( $this, 'apply_images' ), $content ); + $images = $this->apply_images( $matches[0] ); + if ( is_array( $images ) ) { + $_content = array_fill( 0, count( $images ), $content ); + $content = ''; + foreach ( $images as $key => $value ) { + $content .= preg_replace( $re, $value, $_content[ $key ] ); + } + return $content; + } + + return preg_replace( $re, $images, $content ); } /** @@ -188,7 +239,7 @@ public function apply_dynamic_images( $content ) { * * @param array $data Dynamic request. * - * @return string|void + * @return string|string[]|void */ public function apply_images( $data ) { if ( ! isset( $data[0] ) ) { @@ -267,11 +318,28 @@ public function get_image( $data ) { * @param array $data Dynamic request. * @param bool $magic_tags Is a request for Magic Tags. * - * @return string + * @return string|string[] */ public function apply_data( $data, $magic_tags = false ) { $value = $this->get_data( $data, $magic_tags ); + if ( is_array( $value ) ) { + $updated_value = array(); + foreach ( $value as $key => $val ) { + $_value = $this->apply_formatting( $val, $data ); + + if ( isset( $data['default'] ) && false !== strpos( $data['default'], 'apply_dynamic_link( $data['default'], $key ); + if ( ! empty( $link ) ) { + $_value = preg_replace( '/().*?(<\/a>)/', '$1' . $_value . '$2', $link ); + } + } + + $updated_value[] = $_value; + } + return $updated_value; + } + if ( isset( $data['before'] ) || isset( $data['after'] ) ) { $value = $this->apply_formatting( $value, $data ); } @@ -310,7 +378,7 @@ public function apply_formatting( $value, $data ) { * @param array $data Dynamic request. * @param bool $magic_tags Is a request for Magic Tags. * - * @return string + * @return string|string[] */ public function get_data( $data, $magic_tags ) { if ( ! ( defined( 'REST_REQUEST' ) && REST_REQUEST ) || true === $magic_tags ) { @@ -611,10 +679,11 @@ public static function query_string_to_array( $qry ) { * Apply dynamic data. * * @param array $data Dynamic request. + * @param int|null $key Optional key for multiple dynamic links in the same content. * - * @return string + * @return string|string[] */ - public function apply_link( $data ) { + public function apply_link( $data, $key = null ) { $link = $this->get_link( $data ); if ( empty( $link ) ) { @@ -627,6 +696,23 @@ public function apply_link( $data ) { $attrs = 'target="_blank"'; } + if ( is_array( $link ) ) { + $value = array(); + foreach ( $link as $link_item ) { + $value[] = sprintf( + '%s', + esc_url( $link_item ), + $attrs, + wp_kses_post( $data['text'] ) + ); + } + + if ( null !== $key ) { + return isset( $value[ $key ] ) ? $value[ $key ] : ''; + } + + return $value; + } $value = sprintf( '%s', esc_url( $link ), @@ -642,7 +728,7 @@ public function apply_link( $data ) { * * @param array $data Dynamic request. * - * @return string|void + * @return string|string[]|void */ public function apply_link_button( $data ) { if ( ! isset( $data[0] ) ) { @@ -666,7 +752,7 @@ public function apply_link_button( $data ) { * * @param array $data Dynamic request. * - * @return string|void + * @return string|string[]|void */ public function get_link( $data ) { if ( ! isset( $data['type'] ) ) { diff --git a/inc/server/class-dynamic-content-server.php b/inc/server/class-dynamic-content-server.php index 51d41a61c..103e12fbc 100644 --- a/inc/server/class-dynamic-content-server.php +++ b/inc/server/class-dynamic-content-server.php @@ -231,23 +231,31 @@ public function get( $request ) { $path = apply_filters( 'otter_blocks_evaluate_dynamic_content_media_server', $path, $request ); - if ( $size = @getimagesize( $path ) ) { // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged, Squiz.PHP.DisallowMultipleAssignments.FoundInControlStructure - ob_start(); - readfile( $path ); //phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_read_readfile - $output = ob_get_contents(); + $paths = is_array( $path ) ? $path : array( $path ); + $output = ''; + $mime_type = ''; + $mime_locked = false; + + foreach ( $paths as $path ) { + if ( empty( $path ) ) { + continue; + } + + $size = @getimagesize( $path ); - if ( ! empty( $size['mime'] ) ) { - header( 'Content-type: ' . $size['mime'] ); + // Set MIME type only once (first valid image). + if ( ! $mime_locked && is_array( $size ) && ! empty( $size['mime'] ) ) { + $mime_type = $size['mime']; + $mime_locked = true; } - return $output; - } - ob_start(); - readfile( $path ); //phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_read_readfile - $output = ob_get_contents(); + ob_start(); + readfile( $path ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_read_readfile + $output .= ob_get_clean(); + } - if ( isset( $size['mime'] ) ) { - header( 'Content-type: ' . $size['mime'] ); + if ( ! empty( $mime_type ) && ! headers_sent() ) { + header( 'Content-Type: ' . $mime_type ); } return $output; } diff --git a/plugins/otter-pro/inc/plugins/class-dynamic-content.php b/plugins/otter-pro/inc/plugins/class-dynamic-content.php index 65ee6fdbb..d5b3f6ab0 100644 --- a/plugins/otter-pro/inc/plugins/class-dynamic-content.php +++ b/plugins/otter-pro/inc/plugins/class-dynamic-content.php @@ -213,14 +213,25 @@ public function get_post_meta( $data ) { * * @param array $data Dynamic Data. * - * @return string + * @return string|string[] */ public function get_acf( $data ) { $default = isset( $data['default'] ) ? esc_html( $data['default'] ) : ''; $meta = ''; - if ( isset( $data['metaKey'] ) ) { - $meta = get_field( esc_html( $data['metaKey'] ), $data['context'], true ); + if ( ! isset( $data['metaKey'] ) ) { + return $default; + } + + $field_key = esc_html( $data['metaKey'] ); + $meta = get_field( $field_key, $data['context'] ); + + // Get the ACF repeater's sub-field value. + if ( ( null === $meta || false === $meta || '' === $meta ) && function_exists( 'acf_get_field' ) ) { + $sub_value = $this->get_acf_repeater_sub_field( $field_key, $data['context'] ); + if ( null !== $sub_value ) { + return $sub_value; + } } if ( is_array( $meta ) ) { @@ -256,6 +267,177 @@ public function get_acf( $data ) { return esc_html( $meta ); } + /** + * Get ACF Repeater Sub-field Values. + * + * @param string $sub_field_key ACF field key of the targeted sub-field. + * @param int $post_id Post ID. + * @return string[]|null Array of sub-field values, or null on failure. + */ + private function get_acf_repeater_sub_field( $sub_field_key, $post_id ) { + $field = acf_get_field( $sub_field_key ); + if ( ! $field || ! isset( $field['parent'] ) ) { + return null; + } + + $path = array( $field['name'] ); + $cursor = $field; + + while ( true ) { + $parent = acf_get_field( (int) $cursor['parent'] ); + if ( ! $parent ) { + break; + } + array_unshift( $path, $parent['name'] ); + $cursor = $parent; + } + + // The top-level ancestor must be a repeater field. + if ( 'repeater' !== $cursor['type'] ) { + return null; + } + + $rows = get_field( $cursor['key'], $post_id ); + if ( ! is_array( $rows ) || empty( $rows ) ) { + return null; + } + + // $path[0] is the top-level repeater's own name — already consumed by + // get_field(). Pass the remainder as the drill-down path. + $values = $this->collect_acf_sub_field_values( $rows, array_slice( $path, 1 ) ); + + if ( empty( $values ) ) { + return null; + } + + $items = array(); + foreach ( $values as $v ) { + $items[] = esc_html( $v ); + } + + return $items; + } + + /** + * Recursively collect values of a specific sub-field from ACF repeater rows. + * + * @param mixed $rows Repeater rows at the current depth level. + * @param string[] $path Remaining field names leading to the target leaf. + * @return string[] Flat array of unescaped leaf string values. + */ + private function collect_acf_sub_field_values( $rows, $path ) { + if ( empty( $path ) || ! is_array( $rows ) ) { + return array(); + } + + $field_name = array_shift( $path ); + $collected = array(); + + foreach ( $rows as $row ) { + if ( ! is_array( $row ) || ! isset( $row[ $field_name ] ) ) { + continue; + } + + $value = $row[ $field_name ]; + + if ( empty( $path ) ) { + // Leaf node: collect non-empty strings. + if ( is_string( $value ) && '' !== $value ) { + $collected[] = $value; + } + } elseif ( is_array( $value ) ) { + // Intermediate node pointing to a nested repeater. + $collected = array_merge( + $collected, + $this->collect_acf_sub_field_values( $value, $path ) + ); + } + } + + return $collected; + } + + /** + * Get ACF Repeater Sub-field Image Values. + * + * @param string $sub_field_key ACF field key of the image sub-field. + * @param int $post_id Post ID. + * @return mixed[] Flat array of raw image values, or empty array on failure. + */ + private function get_acf_repeater_sub_field_image( $sub_field_key, $post_id ) { + if ( ! function_exists( 'acf_get_field' ) ) { + return array(); + } + + $field = acf_get_field( $sub_field_key ); + if ( ! $field || ! isset( $field['parent'] ) ) { + return array(); + } + + // Walk up the ancestor chain to find the top-level repeater. + $path = array( $field['name'] ); + $cursor = $field; + + while ( true ) { + $parent = acf_get_field( (int) $cursor['parent'] ); + if ( ! $parent ) { + break; + } + array_unshift( $path, $parent['name'] ); + $cursor = $parent; + } + + if ( 'repeater' !== $cursor['type'] ) { + return array(); + } + + $rows = get_field( $cursor['key'], $post_id ); + if ( ! is_array( $rows ) || empty( $rows ) ) { + return array(); + } + + return $this->collect_acf_sub_field_images( $rows, array_slice( $path, 1 ) ); + } + + /** + * Recursively collect image values of a specific sub-field from ACF repeater rows. + * + * @param mixed $rows Repeater rows at the current depth level. + * @param string[] $path Remaining field names leading to the target leaf. + * @return mixed[] Flat array of raw image values. + */ + private function collect_acf_sub_field_images( $rows, $path ) { + if ( empty( $path ) || ! is_array( $rows ) ) { + return array(); + } + + $field_name = array_shift( $path ); + $collected = array(); + + foreach ( $rows as $row ) { + if ( ! is_array( $row ) || ! isset( $row[ $field_name ] ) ) { + continue; + } + + $value = $row[ $field_name ]; + + if ( empty( $path ) ) { + // Leaf node: collect any non-empty image value. + if ( ! empty( $value ) ) { + $collected[] = $value; + } + } elseif ( is_array( $value ) ) { + // Intermediate node: recurse into nested repeater rows. + $collected = array_merge( + $collected, + $this->collect_acf_sub_field_images( $value, $path ) + ); + } + } + + return $collected; + } + /** * Get Author Meta. * @@ -444,7 +626,7 @@ public function get_country( $data ) { * @param \WP_REST_Request $request Request data. * * @since 2.0.9 - * @return string + * @return string|string[] */ public function evaluate_content_media_server( $path, $request ) { $type = $request->get_param( 'type' ); @@ -478,24 +660,21 @@ public function evaluate_content_media_server( $path, $request ) { if ( 'acf' === $type && ! empty( $meta ) && class_exists( 'ACF' ) ) { $field = get_field( $meta, $context ); - if ( ! empty( $field ) ) { - if ( is_array( $field ) && isset( $field['ID'] ) ) { - $path = wp_get_original_image_path( $field['ID'] ); - } - - if ( is_string( $field ) ) { - $image = $this->get_image_id_from_url( $field ); - - if ( $image ) { - $path = wp_get_original_image_path( $image ); - } else { - $path = $field; + // Fall back to repeater sub-field traversal when the field is a + // sub-field inside a repeater (get_field() returns nothing for those). + if ( empty( $field ) ) { + $images = $this->get_acf_repeater_sub_field_image( $meta, $context ); + if ( ! empty( $images ) ) { + $path = array(); + foreach ( $images as $image ) { + $path[] = $this->get_server_attachment_path( $image ); } + return $path; } + } - if ( is_int( $field ) ) { - $path = wp_get_original_image_path( $field ); - } + if ( ! empty( $field ) ) { + $path = $this->get_server_attachment_path( $field ); } } @@ -509,7 +688,7 @@ public function evaluate_content_media_server( $path, $request ) { * @param array $data Request data. * * @since 2.0.9 - * @return string + * @return string|string[] */ public function evaluate_content_media_content( $value, $data ) { if ( 'postMeta' === $data['type'] && isset( $data['meta'] ) && ! empty( $data['meta'] ) ) { @@ -538,18 +717,21 @@ public function evaluate_content_media_content( $value, $data ) { if ( 'acf' === $data['type'] && ! empty( $data['meta'] ) && class_exists( 'ACF' ) ) { $field = get_field( $data['meta'], $data['context'] ); - if ( ! empty( $field ) ) { - if ( is_array( $field ) && isset( $field['url'] ) ) { - $value = $field['url']; - } - - if ( is_string( $field ) ) { - $value = $field; + // Fall back to repeater sub-field traversal when the field is a + // sub-field inside a repeater (get_field() returns nothing for those). + if ( empty( $field ) ) { + $images = $this->get_acf_repeater_sub_field_image( $data['meta'], $data['context'] ); + if ( ! empty( $images ) ) { + $value = array(); + foreach ( $images as $image ) { + $value[] = $this->get_attachment_url( $image ); + } + return $value; } + } - if ( is_int( $field ) ) { - $value = wp_get_attachment_image_url( $field, 'full' ); - } + if ( ! empty( $field ) ) { + $value = $this->get_attachment_url( $field ); } } @@ -585,6 +767,58 @@ public function get_image_id_from_url( $url ) { return false; } + /** + * Get attachment URL from ACF field value. + * + * @param mixed $field ACF field value. + * @return string URL of the attachment, or empty string if it cannot be determined. + */ + private function get_attachment_url( $field ) { + $value = ''; + if ( is_array( $field ) && isset( $field['url'] ) ) { + $value = $field['url']; + } + + if ( is_string( $field ) ) { + $value = $field; + } + + if ( is_int( $field ) ) { + $value = wp_get_attachment_image_url( $field, 'full' ); + } + + return esc_url( $value ); + } + + /** + * Get attchament path. + * + * @param mixed $field ACF field value. + * @return string URL of the attachment, or empty string if it cannot be determined. + */ + private function get_server_attachment_path( $field ) { + $path = ''; + if ( is_array( $field ) && isset( $field['ID'] ) ) { + $path = wp_get_original_image_path( $field['ID'] ); + } + + if ( is_string( $field ) ) { + $image = $this->get_image_id_from_url( $field ); + + if ( $image ) { + $path = wp_get_original_image_path( $image ); + } else { + $path = $field; + } + } + + if ( is_int( $field ) ) { + $path = wp_get_original_image_path( $field ); + } + + return esc_url( $path ); + } + /** * The instance method for the static class. * Defines and returns the instance of the static class. diff --git a/src/pro/components/acf-image-select/index.js b/src/pro/components/acf-image-select/index.js index ceeb215ce..8c430ecdb 100644 --- a/src/pro/components/acf-image-select/index.js +++ b/src/pro/components/acf-image-select/index.js @@ -4,37 +4,66 @@ import { __ } from '@wordpress/i18n'; import { + BaseControl, Placeholder, - SelectControl, Spinner } from '@wordpress/components'; import { useSelect } from '@wordpress/data'; +const ALLOWED_ACF_IMAGE_TYPES = [ 'image', 'url' ]; + +const FIELD_INDENT = '\u00A0\u00A0\u00A0'; + +/** + * Recursively flatten ACF fields into + + { groups.map( group => ( + + { flattenACFImageOptions( group?.fields || [] ) } + + ) ) } + + ); }; diff --git a/src/pro/plugins/data/index.js b/src/pro/plugins/data/index.js index 087d30ff6..d05f47e1c 100644 --- a/src/pro/plugins/data/index.js +++ b/src/pro/plugins/data/index.js @@ -148,20 +148,32 @@ registerStore( 'otter-pro', { if ( data?.success ) { const { groups } = data; - const fields = groups - ?.map( ({ fields, data }) => { - return fields.map( field => { - field.urlLocation = `${ window.themeisleGutenberg?.rootUrl || '' }/wp-admin/post.php?post=${ data.ID }&action=edit`; - return field; - }); - }) - .flat() - .reduce( ( acc, field ) => { + + /** + * Recursively flattens ACF fields, including sub-fields, into a single-level object keyed by field key. + * + * @param {Array} fieldList - Array of ACF field objects. + * @param {string} urlLocation - Admin edit URL for the parent group. + * @param {Object} acc - Accumulator object. + * @return {Object} The populated accumulator. + */ + const flattenFields = ( fieldList, urlLocation, acc = {} ) => { + fieldList.forEach( field => { if ( field.key && field.label ) { - acc[ field.key ] = pick( field, [ 'label', 'type', 'prepend', 'append', 'default_value', 'value', 'urlLocation' ]); + field.urlLocation = urlLocation; + acc[ field.key ] = pick( field, [ 'label', 'type', 'prepend', 'append', 'default_value', 'value', 'urlLocation', 'sub_fields' ]); + } + if ( field.sub_fields?.length ) { + flattenFields( field.sub_fields, urlLocation, acc ); } - return acc; - }, {}); + }); + return acc; + }; + + const fields = groups?.reduce( ( acc, { fields: groupFields, data: groupData }) => { + const urlLocation = `${ window.themeisleGutenberg?.rootUrl || '' }/wp-admin/post.php?post=${ groupData.ID }&action=edit`; + return flattenFields( groupFields, urlLocation, acc ); + }, {}); return actions.setACFData( groups, fields, true ); } diff --git a/src/pro/plugins/dynamic-content/link-edit.js b/src/pro/plugins/dynamic-content/link-edit.js index c56a31de0..459e7bb4c 100644 --- a/src/pro/plugins/dynamic-content/link-edit.js +++ b/src/pro/plugins/dynamic-content/link-edit.js @@ -18,6 +18,46 @@ const ALLOWED_ACF_TYPES = [ 'url' ]; +/** + * Recursively flatten ACF fields into , + // Sub-fields indented one level deeper. + ...flattenACFOptions( subFields || [], depth + 1 ) + ]; + } + + if ( ALLOWED_ACF_TYPES.includes( type ) ) { + return [ + + ]; + } + + return []; + } ); +}; + const Edit = ({ attributes, changeAttributes @@ -255,16 +297,7 @@ const Edit = ({ key={ group?.data?.key } label={ group?.data?.title } > - { group?.fields - ?.filter( ({ key, label, type }) => key && label && ALLOWED_ACF_TYPES.includes( type ) ) - .map( ({ key, label }) => ( - - ) ) } + { flattenACFOptions( group?.fields || [] ) } ); }) } From 10730ae2cf040bd56ad0fa5987b2440df2be28db Mon Sep 17 00:00:00 2001 From: girishpanchal30 Date: Wed, 15 Apr 2026 18:52:38 +0530 Subject: [PATCH 2/3] fix: phpcs --- inc/plugins/class-dynamic-content.php | 4 ++-- inc/server/class-dynamic-content-server.php | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/inc/plugins/class-dynamic-content.php b/inc/plugins/class-dynamic-content.php index af2f121a9..60a19bc53 100644 --- a/inc/plugins/class-dynamic-content.php +++ b/inc/plugins/class-dynamic-content.php @@ -151,7 +151,7 @@ public function apply_magic_tags( $data ) { /** * Filter post content for dynamic link. * - * @param string $content Post content. + * @param string $content Post content. * @param int|null $key Optional key for multiple dynamic links in the same content. * * @return string @@ -678,7 +678,7 @@ public static function query_string_to_array( $qry ) { /** * Apply dynamic data. * - * @param array $data Dynamic request. + * @param array $data Dynamic request. * @param int|null $key Optional key for multiple dynamic links in the same content. * * @return string|string[] diff --git a/inc/server/class-dynamic-content-server.php b/inc/server/class-dynamic-content-server.php index 103e12fbc..cbb1b5cbb 100644 --- a/inc/server/class-dynamic-content-server.php +++ b/inc/server/class-dynamic-content-server.php @@ -241,7 +241,7 @@ public function get( $request ) { continue; } - $size = @getimagesize( $path ); + $size = @getimagesize( $path ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged // Set MIME type only once (first valid image). if ( ! $mime_locked && is_array( $size ) && ! empty( $size['mime'] ) ) { From f17e161b2004ed4cbe4c2c814c4d023e1c3d2c6b Mon Sep 17 00:00:00 2001 From: girishpanchal30 Date: Thu, 16 Apr 2026 11:04:09 +0530 Subject: [PATCH 3/3] enhance dynamic content handling --- inc/plugins/class-dynamic-content.php | 94 +++++++++++++++---- inc/server/class-dynamic-content-server.php | 34 +++---- .../inc/plugins/class-dynamic-content.php | 55 ++++------- src/pro/components/acf-image-select/index.js | 44 +-------- src/pro/helpers/acf-field-options.js | 59 ++++++++++++ src/pro/plugins/dynamic-content/link-edit.js | 44 +-------- src/pro/plugins/dynamic-content/value-edit.js | 46 +-------- 7 files changed, 177 insertions(+), 199 deletions(-) create mode 100644 src/pro/helpers/acf-field-options.js diff --git a/inc/plugins/class-dynamic-content.php b/inc/plugins/class-dynamic-content.php index 60a19bc53..2bf17a902 100644 --- a/inc/plugins/class-dynamic-content.php +++ b/inc/plugins/class-dynamic-content.php @@ -169,17 +169,47 @@ public function apply_dynamic_link( $content, $key = null ) { return $content; } - $link = $this->apply_link( $matches[0], $key ); - - if ( is_array( $link ) ) { - $_content = array_fill( 0, count( $link ), $content ); - $content = ''; - foreach ( $link as $key => $value ) { - $content .= preg_replace( $re, $value, $_content[ $key ] ); + $resolved = array(); + $clone_count = 1; + foreach ( $matches as $match ) { + $value = $this->apply_link( $match, $key ); + $resolved[] = $value; + if ( is_array( $value ) ) { + $clone_count = max( $clone_count, count( $value ) ); } - return $content; } - return preg_replace( $re, $link, $content, 1 ); + + // Replace the content for multiple dynamic links in the same content. + if ( $clone_count > 1 ) { + $output = ''; + for ( $i = 0; $i < $clone_count; $i++ ) { + $clone = $content; + foreach ( $matches as $j => $match ) { + $value = $resolved[ $j ]; + $replacement = is_array( $value ) + ? ( isset( $value[ $i ] ) ? $value[ $i ] : '' ) + : $value; + $pos = strpos( $clone, $match[0] ); + if ( false !== $pos ) { + $clone = substr_replace( $clone, $replacement, $pos, strlen( $match[0] ) ); + } + } + $output .= $clone; + } + return $output; + } + + // Replace the content for single dynamic link in the content. + $index = 0; + return preg_replace_callback( + $re, + function ( $data ) use ( &$resolved, &$index ) { + $value = isset( $resolved[ $index ] ) ? $resolved[ $index ] : $data[0]; + $index++; + return is_string( $value ) ? $value : $data[0]; + }, + $content + ); } /** @@ -221,17 +251,47 @@ public function apply_dynamic_images( $content ) { return $content; } - $images = $this->apply_images( $matches[0] ); - if ( is_array( $images ) ) { - $_content = array_fill( 0, count( $images ), $content ); - $content = ''; - foreach ( $images as $key => $value ) { - $content .= preg_replace( $re, $value, $_content[ $key ] ); + $resolved = array(); + $clone_count = 1; + foreach ( $matches as $match ) { + $value = $this->apply_images( $match ); + $resolved[] = $value; + if ( is_array( $value ) ) { + $clone_count = max( $clone_count, count( $value ) ); } - return $content; } - return preg_replace( $re, $images, $content ); + // Replace the content for multiple dynamic images in the same content. + if ( $clone_count > 1 ) { + $output = ''; + for ( $i = 0; $i < $clone_count; $i++ ) { + $clone = $content; + foreach ( $matches as $j => $match ) { + $value = $resolved[ $j ]; + $replacement = is_array( $value ) + ? ( isset( $value[ $i ] ) ? $value[ $i ] : '' ) + : $value; + $pos = strpos( $clone, $match[0] ); + if ( false !== $pos ) { + $clone = substr_replace( $clone, $replacement, $pos, strlen( $match[0] ) ); + } + } + $output .= $clone; + } + return $output; + } + + // Replace the content for single dynamic image in the content. + $index = 0; + return preg_replace_callback( + $re, + function ( $data ) use ( &$resolved, &$index ) { + $value = isset( $resolved[ $index ] ) ? $resolved[ $index ] : $data[0]; + $index++; + return is_string( $value ) ? $value : $data[0]; + }, + $content + ); } /** diff --git a/inc/server/class-dynamic-content-server.php b/inc/server/class-dynamic-content-server.php index cbb1b5cbb..51d41a61c 100644 --- a/inc/server/class-dynamic-content-server.php +++ b/inc/server/class-dynamic-content-server.php @@ -231,31 +231,23 @@ public function get( $request ) { $path = apply_filters( 'otter_blocks_evaluate_dynamic_content_media_server', $path, $request ); - $paths = is_array( $path ) ? $path : array( $path ); - $output = ''; - $mime_type = ''; - $mime_locked = false; - - foreach ( $paths as $path ) { - if ( empty( $path ) ) { - continue; - } - - $size = @getimagesize( $path ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged + if ( $size = @getimagesize( $path ) ) { // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged, Squiz.PHP.DisallowMultipleAssignments.FoundInControlStructure + ob_start(); + readfile( $path ); //phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_read_readfile + $output = ob_get_contents(); - // Set MIME type only once (first valid image). - if ( ! $mime_locked && is_array( $size ) && ! empty( $size['mime'] ) ) { - $mime_type = $size['mime']; - $mime_locked = true; + if ( ! empty( $size['mime'] ) ) { + header( 'Content-type: ' . $size['mime'] ); } - - ob_start(); - readfile( $path ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_read_readfile - $output .= ob_get_clean(); + return $output; } - if ( ! empty( $mime_type ) && ! headers_sent() ) { - header( 'Content-Type: ' . $mime_type ); + ob_start(); + readfile( $path ); //phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_read_readfile + $output = ob_get_contents(); + + if ( isset( $size['mime'] ) ) { + header( 'Content-type: ' . $size['mime'] ); } return $output; } diff --git a/plugins/otter-pro/inc/plugins/class-dynamic-content.php b/plugins/otter-pro/inc/plugins/class-dynamic-content.php index d5b3f6ab0..6ed7596e9 100644 --- a/plugins/otter-pro/inc/plugins/class-dynamic-content.php +++ b/plugins/otter-pro/inc/plugins/class-dynamic-content.php @@ -626,7 +626,7 @@ public function get_country( $data ) { * @param \WP_REST_Request $request Request data. * * @since 2.0.9 - * @return string|string[] + * @return string */ public function evaluate_content_media_server( $path, $request ) { $type = $request->get_param( 'type' ); @@ -665,16 +665,28 @@ public function evaluate_content_media_server( $path, $request ) { if ( empty( $field ) ) { $images = $this->get_acf_repeater_sub_field_image( $meta, $context ); if ( ! empty( $images ) ) { - $path = array(); - foreach ( $images as $image ) { - $path[] = $this->get_server_attachment_path( $image ); - } - return $path; + $field = $images[0]; } } if ( ! empty( $field ) ) { - $path = $this->get_server_attachment_path( $field ); + if ( is_array( $field ) && isset( $field['ID'] ) ) { + $path = wp_get_original_image_path( $field['ID'] ); + } + + if ( is_string( $field ) ) { + $image = $this->get_image_id_from_url( $field ); + + if ( $image ) { + $path = wp_get_original_image_path( $image ); + } else { + $path = $field; + } + } + + if ( is_int( $field ) ) { + $path = wp_get_original_image_path( $field ); + } } } @@ -790,35 +802,6 @@ private function get_attachment_url( $field ) { return esc_url( $value ); } - /** - * Get attchament path. - * - * @param mixed $field ACF field value. - * @return string URL of the attachment, or empty string if it cannot be determined. - */ - private function get_server_attachment_path( $field ) { - $path = ''; - if ( is_array( $field ) && isset( $field['ID'] ) ) { - $path = wp_get_original_image_path( $field['ID'] ); - } - - if ( is_string( $field ) ) { - $image = $this->get_image_id_from_url( $field ); - - if ( $image ) { - $path = wp_get_original_image_path( $image ); - } else { - $path = $field; - } - } - - if ( is_int( $field ) ) { - $path = wp_get_original_image_path( $field ); - } - - return esc_url( $path ); - } - /** * The instance method for the static class. * Defines and returns the instance of the static class. diff --git a/src/pro/components/acf-image-select/index.js b/src/pro/components/acf-image-select/index.js index 8c430ecdb..dce04261f 100644 --- a/src/pro/components/acf-image-select/index.js +++ b/src/pro/components/acf-image-select/index.js @@ -11,47 +11,9 @@ import { import { useSelect } from '@wordpress/data'; -const ALLOWED_ACF_IMAGE_TYPES = [ 'image', 'url' ]; - -const FIELD_INDENT = '\u00A0\u00A0\u00A0'; - -/** - * Recursively flatten ACF fields into is + * invalid HTML and not supported by browsers). + * + * @param {Array} fields ACF field objects at the current nesting level. + * @param {string[]} allowedTypes ACF field types to render as selectable , + // Sub-fields indented one level deeper. + ...flattenACFFieldOptions( subFields || [], allowedTypes, depth + 1 ) + ]; + } + + if ( allowedTypes.includes( type ) ) { + return [ + + ]; + } + + return []; + } ); +}; diff --git a/src/pro/plugins/dynamic-content/link-edit.js b/src/pro/plugins/dynamic-content/link-edit.js index 459e7bb4c..4ad0a68e0 100644 --- a/src/pro/plugins/dynamic-content/link-edit.js +++ b/src/pro/plugins/dynamic-content/link-edit.js @@ -14,50 +14,12 @@ import { useSelect } from '@wordpress/data'; import { Fragment } from '@wordpress/element'; +import { flattenACFFieldOptions } from '../../helpers/acf-field-options'; + const ALLOWED_ACF_TYPES = [ 'url' ]; -/** - * Recursively flatten ACF fields into , - // Sub-fields indented one level deeper. - ...flattenACFOptions( subFields || [], depth + 1 ) - ]; - } - - if ( ALLOWED_ACF_TYPES.includes( type ) ) { - return [ - - ]; - } - - return []; - } ); -}; - const Edit = ({ attributes, changeAttributes @@ -297,7 +257,7 @@ const Edit = ({ key={ group?.data?.key } label={ group?.data?.title } > - { flattenACFOptions( group?.fields || [] ) } + { flattenACFFieldOptions( group?.fields || [], ALLOWED_ACF_TYPES ) } ); }) }