From 24a1af35f08336d5d3fd9e56fd38270dc7abb621 Mon Sep 17 00:00:00 2001 From: girishpanchal30 Date: Tue, 21 Jul 2026 13:15:00 +0530 Subject: [PATCH 01/10] feat: support XML and CSV exports for form submissions --- inc/plugins/class-dashboard.php | 116 ++++++++++++++++++++-- inc/plugins/class-form-records-export.php | 111 ++++++++++++++++++++- src/blocks/test/e2e/blocks/form.spec.js | 19 +++- tests/test-form-submissions.php | 88 +++++++++++++++- 4 files changed, 322 insertions(+), 12 deletions(-) diff --git a/inc/plugins/class-dashboard.php b/inc/plugins/class-dashboard.php index a3f4a8085..a43edc014 100644 --- a/inc/plugins/class-dashboard.php +++ b/inc/plugins/class-dashboard.php @@ -635,6 +635,54 @@ private function the_otter_banner() { max-height: 35px; } + .o-export-split { + position: relative; + display: inline-flex; + } + + .o-export-split #export-submissions { + border-top-right-radius: 0; + border-bottom-right-radius: 0; + } + + .o-export-split__toggle { + border-top-left-radius: 0 !important; + border-bottom-left-radius: 0 !important; + border-left: none !important; + padding: 0 6px !important; + } + + .o-export-split__menu { + position: absolute; + top: 100%; + right: 0; + z-index: 10; + margin: 4px 0 0; + padding: 4px 0; + list-style: none; + background: #fff; + border: 1px solid #c3c4c7; + border-radius: 4px; + box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15); + min-width: 220px; + } + + .o-export-split__item { + display: block; + width: 100%; + padding: 8px 12px; + background: none; + border: none; + text-align: left; + cursor: pointer; + font-size: 13px; + } + + .o-export-split__item:hover, + .o-export-split__item:focus { + background: #f0f0f1; + } + .wp-core-ui .button.o-locked-action, .wp-core-ui .button.o-locked-action:focus { display: inline-flex; @@ -683,9 +731,25 @@ private function the_otter_banner() {

- +
+ + + +
@@ -706,7 +770,20 @@ class="button o-locked-action" export_csv() : $this->export_xml(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + wp_die(); + } + + /** + * Build the WordPress WXR (XML) export of all submissions. + * + * @return string + */ + private function export_xml() { require_once ABSPATH . 'wp-admin/includes/export.php'; + ob_start(); export_wp( array( 'content' => Form_Submissions::FORM_RECORD_TYPE ) ); $export = ob_get_clean(); - echo ent2ncr( $export ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped - wp_die(); + return ent2ncr( $export ); + } + + /** + * Build a CSV export of all submissions. + * + * @return string + */ + private function export_csv() { + $records = get_posts( + array( + 'post_type' => Form_Submissions::FORM_RECORD_TYPE, + 'post_status' => array( 'draft', 'unread', 'read', 'trash' ), + 'posts_per_page' => -1, + 'orderby' => 'ID', + 'order' => 'ASC', + ) + ); + + update_meta_cache( 'post', wp_list_pluck( $records, 'ID' ) ); + + $fixed_columns = array( + 'id' => __( 'ID', 'otter-blocks' ), + 'status' => __( 'Status', 'otter-blocks' ), + 'date' => __( 'Submission Date', 'otter-blocks' ), + 'form' => __( 'Form', 'otter-blocks' ), + 'post' => __( 'Post URL', 'otter-blocks' ), + ); + + $input_columns = array(); + $rows = array(); + + foreach ( $records as $record ) { + $post_id = $record->ID; + $meta = get_post_meta( $post_id, Form_Submissions::FORM_RECORD_META_KEY, true ); + + if ( ! is_array( $meta ) ) { + continue; + } + + $row = array( + 'id' => substr( strval( $post_id ), -8 ), + 'status' => get_post_status( $post_id ), + 'date' => get_the_date( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ), $post_id ), + 'form' => isset( $meta['form']['value'] ) ? $meta['form']['value'] : '', + 'post' => isset( $meta['post_url']['value'] ) ? $meta['post_url']['value'] : '', + ); + + $inputs = isset( $meta['inputs'] ) && is_array( $meta['inputs'] ) ? $meta['inputs'] : array(); + + foreach ( $inputs as $input ) { + if ( empty( $input ) || ! isset( $input['type'], $input['label'] ) || 'stripe-field' === $input['type'] ) { + continue; + } + + $label = $input['label']; + + if ( ! isset( $input_columns[ $label ] ) ) { + $input_columns[ $label ] = $label; + } + + $value = isset( $input['value'] ) ? $input['value'] : ''; + + if ( 'file' === $input['type'] && isset( $input['metadata']['name'] ) ) { + $value = $input['metadata']['name']; + } + + $row[ $label ] = $value; + } + + $rows[] = $row; + } + + $columns = array_merge( $fixed_columns, $input_columns ); + + $stream = fopen( 'php://temp', 'w+' ); + + fputcsv( $stream, array_values( $columns ) ); + + foreach ( $rows as $row ) { + $line = array(); + + foreach ( array_keys( $columns ) as $key ) { + $line[] = isset( $row[ $key ] ) ? $row[ $key ] : ''; + } + + fputcsv( $stream, $line ); + } + + rewind( $stream ); + $csv = stream_get_contents( $stream ); + fclose( $stream ); + + // Prefix a UTF-8 BOM so Excel detects the encoding instead of mangling accented characters. + return "\xEF\xBB\xBF" . $csv; } } diff --git a/src/blocks/test/e2e/blocks/form.spec.js b/src/blocks/test/e2e/blocks/form.spec.js index aec3dee3b..82815e4cf 100644 --- a/src/blocks/test/e2e/blocks/form.spec.js +++ b/src/blocks/test/e2e/blocks/form.spec.js @@ -412,10 +412,10 @@ test.describe( 'Form Block', () => { await expect( page.locator( '.otter-form-input[type="hidden"]' ) ).toHaveValue( '123' ); }); - test( 'can export form data', async({ page }) => { + test( 'can export form data as WordPress XML (WXR)', async({ page }) => { await page.goto( '/wp-admin/edit.php?post_type=otter_form_record' ); - const exportBtn = page.getByRole( 'button', { name: 'Export' }); + const exportBtn = page.getByRole( 'button', { name: 'Export', exact: true }); await expect( exportBtn ).toBeVisible(); @@ -425,5 +425,20 @@ test.describe( 'Form Block', () => { await download.path(); // Wait for download to complete. expect( download.suggestedFilename().startsWith( 'otter_form_submissions' ) ).toBeTruthy(); + expect( download.suggestedFilename().endsWith( '.xml' ) ).toBeTruthy(); + }); + + test( 'can export form data as CSV', async({ page }) => { + await page.goto( '/wp-admin/edit.php?post_type=otter_form_record' ); + + await page.locator( '#export-submissions-toggle' ).click(); + + const downloadPromise = page.waitForEvent( 'download' ); + await page.getByRole( 'button', { name: 'Export as CSV' }).click(); + const download = await downloadPromise; + + await download.path(); // Wait for download to complete. + expect( download.suggestedFilename().startsWith( 'otter_form_submissions' ) ).toBeTruthy(); + expect( download.suggestedFilename().endsWith( '.csv' ) ).toBeTruthy(); }); }); diff --git a/tests/test-form-submissions.php b/tests/test-form-submissions.php index 68a0aea73..1629833e9 100644 --- a/tests/test-form-submissions.php +++ b/tests/test-form-submissions.php @@ -58,7 +58,7 @@ public function set_up() { */ public function tear_down() { unset( $_REQUEST[ Form_Submissions::FORM_RECORD_TYPE ], $_REQUEST['_wpnonce'], $_REQUEST['post'] ); - unset( $_POST['action'], $_POST['_wpnonce'], $_POST['_nonce'] ); + unset( $_POST['action'], $_POST['_wpnonce'], $_POST['_nonce'], $_POST['format'] ); unset( $_GET['post_type'], $_GET['filter_action'], $_GET['filters_nonce'], $_GET['otter_form_filter'], $_REQUEST['otter_form_filter'] ); unset( $_GET['otter_post_filter'], $_REQUEST['otter_post_filter'] ); unset( $GLOBALS['otter_test_stripe_record_id'] ); @@ -809,6 +809,92 @@ public function test_export_requires_manage_options() { ( new Form_Records_Export() )->export_submissions(); } + /** + * Ensure the export defaults to the WordPress XML (WXR) format when no format is + * posted, preserving the pre-existing endpoint contract. + */ + public function test_export_defaults_to_xml_format() { + if ( ! \ThemeIsle\GutenbergBlocks\Pro::is_pro_installed() ) { + $this->markTestSkipped( 'Otter Pro is not installed in this test environment.' ); + } + + add_filter( 'product_otter_license_status', array( $this, 'valid_license' ) ); + + $this->create_record(); + + wp_set_current_user( $this->admin_id ); + $_POST['_nonce'] = wp_create_nonce( 'otter_form_export_submissions' ); + + ob_start(); + + try { + ( new Form_Records_Export() )->export_submissions(); + } catch ( WPDieException $exception ) { + // export_submissions() always ends in wp_die(); assertions run on the buffered output below. + } + + $output = ob_get_clean(); + + $this->assertStringContainsString( 'markTestSkipped( 'Otter Pro is not installed in this test environment.' ); + } + + add_filter( 'product_otter_license_status', array( $this, 'valid_license' ) ); + + $this->create_record( + 'unread', + array( + 'inputs' => array( + 'input01' => array( + 'label' => 'Name', + 'value' => 'Ada Lovelace', + 'type' => 'text', + ), + ), + ) + ); + + $this->create_record( + 'read', + array( + 'inputs' => array( + 'input02' => array( + 'label' => 'Company', + 'value' => 'Analytical Engines Ltd', + 'type' => 'text', + ), + ), + ) + ); + + wp_set_current_user( $this->admin_id ); + $_POST['_nonce'] = wp_create_nonce( 'otter_form_export_submissions' ); + $_POST['format'] = 'csv'; + + ob_start(); + + try { + ( new Form_Records_Export() )->export_submissions(); + } catch ( WPDieException $exception ) { + // export_submissions() always ends in wp_die(); assertions run on the buffered output below. + } + + $output = ob_get_clean(); + + $this->assertStringContainsString( 'Name', $output ); + $this->assertStringContainsString( 'Company', $output ); + $this->assertStringContainsString( 'Ada Lovelace', $output ); + $this->assertStringContainsString( 'Analytical Engines Ltd', $output ); + } + /** * Ensure the list-table bulk actions match the current status view. */ From b359a3b3f8b8cd7c2825a4ef52669129aa17cc61 Mon Sep 17 00:00:00 2001 From: girishpanchal30 Date: Tue, 21 Jul 2026 14:39:13 +0530 Subject: [PATCH 02/10] fix: sanitize CSV output --- inc/plugins/class-dashboard.php | 2 +- inc/plugins/class-form-records-export.php | 19 +++++++++++++------ 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/inc/plugins/class-dashboard.php b/inc/plugins/class-dashboard.php index a43edc014..038be1fba 100644 --- a/inc/plugins/class-dashboard.php +++ b/inc/plugins/class-dashboard.php @@ -739,7 +739,7 @@ private function the_otter_banner() { id="export-submissions-toggle" type="button" class="button o-export-split__toggle" - aria-haspopup="true" + aria-controls="export-submissions-menu" aria-expanded="false" aria-label="" > diff --git a/inc/plugins/class-form-records-export.php b/inc/plugins/class-form-records-export.php index 59fb098fa..cb4e35650 100644 --- a/inc/plugins/class-form-records-export.php +++ b/inc/plugins/class-form-records-export.php @@ -72,7 +72,7 @@ private function export_csv() { $records = get_posts( array( 'post_type' => Form_Submissions::FORM_RECORD_TYPE, - 'post_status' => array( 'draft', 'unread', 'read', 'trash' ), + 'post_status' => array( 'draft', 'unread', 'read', 'trash', 'publish' ), 'posts_per_page' => -1, 'orderby' => 'ID', 'order' => 'ASC', @@ -115,10 +115,11 @@ private function export_csv() { continue; } - $label = $input['label']; + $label = $input['label']; + $column_key = 'input:' . $label; if ( ! isset( $input_columns[ $label ] ) ) { - $input_columns[ $label ] = $label; + $input_columns[ $column_key ] = $label; } $value = isset( $input['value'] ) ? $input['value'] : ''; @@ -127,7 +128,7 @@ private function export_csv() { $value = $input['metadata']['name']; } - $row[ $label ] = $value; + $row[ $column_key ] = $value; } $rows[] = $row; @@ -137,13 +138,19 @@ private function export_csv() { $stream = fopen( 'php://temp', 'w+' ); - fputcsv( $stream, array_values( $columns ) ); + $sanitize_cell = static function ( $value ) { + $value = strval( $value ); + return preg_match( '/^[\x00-\x20]*[=+\-@]/', $value ) ? "'" . $value : $value; + }; + + fputcsv( $stream, array_map( $sanitize_cell, array_values( $columns ) ) ); foreach ( $rows as $row ) { $line = array(); foreach ( array_keys( $columns ) as $key ) { - $line[] = isset( $row[ $key ] ) ? $row[ $key ] : ''; + $value = isset( $row[ $key ] ) ? $row[ $key ] : ''; + $line[] = $sanitize_cell( $value ); } fputcsv( $stream, $line ); From ad6e823ca790d4053147898f168b10b167ee230a Mon Sep 17 00:00:00 2001 From: girishpanchal30 Date: Tue, 21 Jul 2026 14:54:06 +0530 Subject: [PATCH 03/10] fix: phpcs --- inc/plugins/class-form-records-export.php | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/inc/plugins/class-form-records-export.php b/inc/plugins/class-form-records-export.php index cb4e35650..d777c70af 100644 --- a/inc/plugins/class-form-records-export.php +++ b/inc/plugins/class-form-records-export.php @@ -116,7 +116,7 @@ private function export_csv() { } $label = $input['label']; - $column_key = 'input:' . $label; + $column_key = 'input:' . $label; if ( ! isset( $input_columns[ $label ] ) ) { $input_columns[ $column_key ] = $label; @@ -136,24 +136,24 @@ private function export_csv() { $columns = array_merge( $fixed_columns, $input_columns ); - $stream = fopen( 'php://temp', 'w+' ); + $stream = fopen( 'php://temp', 'w+' ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fopen $sanitize_cell = static function ( $value ) { - $value = strval( $value ); - return preg_match( '/^[\x00-\x20]*[=+\-@]/', $value ) ? "'" . $value : $value; - }; + $value = strval( $value ); + return preg_match( '/^[\x00-\x20]*[=+\-@]/', $value ) ? "'" . $value : $value; + }; - fputcsv( $stream, array_map( $sanitize_cell, array_values( $columns ) ) ); + fputcsv( $stream, array_map( $sanitize_cell, array_values( $columns ) ) ); // phpcs:ignore WordPressVIPMinimum.Functions.RestrictedFunctions.file_ops_fputcsv foreach ( $rows as $row ) { $line = array(); foreach ( array_keys( $columns ) as $key ) { $value = isset( $row[ $key ] ) ? $row[ $key ] : ''; - $line[] = $sanitize_cell( $value ); + $line[] = $sanitize_cell( $value ); } - fputcsv( $stream, $line ); + fputcsv( $stream, $line ); // phpcs:ignore WordPressVIPMinimum.Functions.RestrictedFunctions.file_ops_fputcsv } rewind( $stream ); From 1d3ae248f920eae80ae5daf0b9bb67de927bb516 Mon Sep 17 00:00:00 2001 From: girishpanchal30 Date: Wed, 29 Jul 2026 10:39:32 +0530 Subject: [PATCH 04/10] feat: enhance CSV export --- inc/plugins/class-form-records-export.php | 235 ++++++++++++++++------ tests/test-form-submissions.php | 58 ++++++ 2 files changed, 234 insertions(+), 59 deletions(-) diff --git a/inc/plugins/class-form-records-export.php b/inc/plugins/class-form-records-export.php index d777c70af..c02f38460 100644 --- a/inc/plugins/class-form-records-export.php +++ b/inc/plugins/class-form-records-export.php @@ -16,6 +16,13 @@ * Class Form_Records_Export */ class Form_Records_Export { + /** + * The number of submissions to fetch in each batch when exporting to CSV. + * + * @var int + */ + const EXPORT_BATCH_SIZE = 100; + /** * Register hooks. * @@ -44,7 +51,12 @@ public function export_submissions() { $format = isset( $_POST['format'] ) ? sanitize_key( wp_unslash( $_POST['format'] ) ) : 'xml'; // phpcs:ignore WordPress.Security.NonceVerification.Recommended - echo 'csv' === $format ? $this->export_csv() : $this->export_xml(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + if ( 'csv' === $format ) { + $this->export_csv(); + } else { + echo $this->export_xml(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + } + wp_die(); } @@ -66,101 +78,206 @@ private function export_xml() { /** * Build a CSV export of all submissions. * - * @return string + * @return void */ private function export_csv() { - $records = get_posts( - array( - 'post_type' => Form_Submissions::FORM_RECORD_TYPE, - 'post_status' => array( 'draft', 'unread', 'read', 'trash', 'publish' ), - 'posts_per_page' => -1, - 'orderby' => 'ID', - 'order' => 'ASC', - ) + $columns = array_merge( $this->get_fixed_columns(), $this->collect_input_columns() ); + + $stream = fopen( 'php://output', 'w' ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fopen + + if ( false === $stream ) { + return; + } + + // Prefix a UTF-8 BOM so Excel detects the encoding instead of mangling accented characters. + fwrite( $stream, "\xEF\xBB\xBF" ); // phpcs:ignore WordPressVIPMinimum.Functions.RestrictedFunctions.file_ops_fwrite + + fputcsv( $stream, array_map( array( $this, 'sanitize_cell' ), array_values( $columns ) ) ); // phpcs:ignore WordPressVIPMinimum.Functions.RestrictedFunctions.file_ops_fputcsv + + $column_keys = array_keys( $columns ); + + $this->walk_submissions( + function ( $meta, $record ) use ( $stream, $column_keys ) { + $row = $this->get_record_row( $record, $meta ); + $line = array(); + + foreach ( $column_keys as $key ) { + $line[] = $this->sanitize_cell( isset( $row[ $key ] ) ? $row[ $key ] : '' ); + } + + fputcsv( $stream, $line ); // phpcs:ignore WordPressVIPMinimum.Functions.RestrictedFunctions.file_ops_fputcsv + } ); - update_meta_cache( 'post', wp_list_pluck( $records, 'ID' ) ); + fclose( $stream ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose + } - $fixed_columns = array( + /** + * Columns present for every submission, in export order. + * + * @return array Column key => header label. + */ + private function get_fixed_columns() { + return array( 'id' => __( 'ID', 'otter-blocks' ), 'status' => __( 'Status', 'otter-blocks' ), 'date' => __( 'Submission Date', 'otter-blocks' ), 'form' => __( 'Form', 'otter-blocks' ), 'post' => __( 'Post URL', 'otter-blocks' ), ); + } + /** + * Collect the dynamically generated input columns found across every submission. + * + * @return array Column key => header label, in order of first appearance. + */ + private function collect_input_columns() { $input_columns = array(); - $rows = array(); - foreach ( $records as $record ) { - $post_id = $record->ID; - $meta = get_post_meta( $post_id, Form_Submissions::FORM_RECORD_META_KEY, true ); + $this->walk_submissions( + function ( $meta ) use ( &$input_columns ) { + foreach ( $this->get_record_inputs( $meta ) as $column_key => $input ) { + if ( isset( $input_columns[ $column_key ] ) ) { + continue; + } - if ( ! is_array( $meta ) ) { - continue; + $input_columns[ $column_key ] = $input['label']; + } } + ); - $row = array( - 'id' => substr( strval( $post_id ), -8 ), - 'status' => get_post_status( $post_id ), - 'date' => get_the_date( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ), $post_id ), - 'form' => isset( $meta['form']['value'] ) ? $meta['form']['value'] : '', - 'post' => isset( $meta['post_url']['value'] ) ? $meta['post_url']['value'] : '', - ); + return $input_columns; + } - $inputs = isset( $meta['inputs'] ) && is_array( $meta['inputs'] ) ? $meta['inputs'] : array(); + /** + * Run a callback over every submission, loading them in bounded batches. + * + * @param callable $callback Receives the submission record meta and the submission post. + * @return void + */ + private function walk_submissions( $callback ) { + $offset = 0; + $has_more_submissions = false; - foreach ( $inputs as $input ) { - if ( empty( $input ) || ! isset( $input['type'], $input['label'] ) || 'stripe-field' === $input['type'] ) { - continue; - } + do { + $records = $this->get_submissions_batch( $offset ); + $count = count( $records ); - $label = $input['label']; - $column_key = 'input:' . $label; + if ( 0 === $count ) { + return; + } - if ( ! isset( $input_columns[ $label ] ) ) { - $input_columns[ $column_key ] = $label; - } + $ids = wp_list_pluck( $records, 'ID' ); + update_meta_cache( 'post', $ids ); - $value = isset( $input['value'] ) ? $input['value'] : ''; + foreach ( $records as $record ) { + $meta = get_post_meta( $record->ID, Form_Submissions::FORM_RECORD_META_KEY, true ); - if ( 'file' === $input['type'] && isset( $input['metadata']['name'] ) ) { - $value = $input['metadata']['name']; + if ( ! is_array( $meta ) ) { + continue; } - $row[ $column_key ] = $value; + $callback( $meta, $record ); } - $rows[] = $row; - } + foreach ( $ids as $id ) { + wp_cache_delete( $id, 'posts' ); + wp_cache_delete( $id, 'post_meta' ); + } + + unset( $records, $ids ); + + $offset += $count; + $has_more_submissions = self::EXPORT_BATCH_SIZE === $count; + } while ( $has_more_submissions ); + } + + /** + * Fetch a batch of submissions, oldest first. + * + * @param int $offset How many submissions to skip. + * @return \WP_Post[] + */ + private function get_submissions_batch( $offset ) { + return get_posts( + array( + 'post_type' => Form_Submissions::FORM_RECORD_TYPE, + 'post_status' => array( 'draft', 'unread', 'read', 'trash', 'publish' ), + 'posts_per_page' => self::EXPORT_BATCH_SIZE, + 'offset' => $offset, + 'orderby' => 'ID', + 'order' => 'ASC', + 'update_post_meta_cache' => false, + 'update_post_term_cache' => false, + ) + ); + } + + /** + * Build the CSV row of a single submission, keyed by column. + * + * @param \WP_Post $record Submission post. + * @param array $meta Submission record meta. + * @return array + */ + private function get_record_row( $record, $meta ) { + $post_id = $record->ID; + + $row = array( + 'id' => substr( strval( $post_id ), -8 ), + 'status' => get_post_status( $post_id ), + 'date' => get_the_date( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ), $post_id ), + 'form' => isset( $meta['form']['value'] ) ? $meta['form']['value'] : '', + 'post' => isset( $meta['post_url']['value'] ) ? $meta['post_url']['value'] : '', + ); - $columns = array_merge( $fixed_columns, $input_columns ); + foreach ( $this->get_record_inputs( $meta ) as $column_key => $input ) { + $row[ $column_key ] = $input['value']; + } - $stream = fopen( 'php://temp', 'w+' ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fopen + return $row; + } - $sanitize_cell = static function ( $value ) { - $value = strval( $value ); - return preg_match( '/^[\x00-\x20]*[=+\-@]/', $value ) ? "'" . $value : $value; - }; + /** + * Extract the exportable inputs of a submission, keyed by column. + * + * @param array $meta Submission record meta. + * @return array> Column key => array with the `label` and `value` of the input. + */ + private function get_record_inputs( $meta ) { + $inputs = isset( $meta['inputs'] ) && is_array( $meta['inputs'] ) ? $meta['inputs'] : array(); + $parsed = array(); - fputcsv( $stream, array_map( $sanitize_cell, array_values( $columns ) ) ); // phpcs:ignore WordPressVIPMinimum.Functions.RestrictedFunctions.file_ops_fputcsv + foreach ( $inputs as $input ) { + if ( empty( $input ) || ! isset( $input['type'], $input['label'] ) || 'stripe-field' === $input['type'] ) { + continue; + } - foreach ( $rows as $row ) { - $line = array(); + $value = isset( $input['value'] ) ? $input['value'] : ''; - foreach ( array_keys( $columns ) as $key ) { - $value = isset( $row[ $key ] ) ? $row[ $key ] : ''; - $line[] = $sanitize_cell( $value ); + if ( 'file' === $input['type'] && isset( $input['metadata']['name'] ) ) { + $value = $input['metadata']['name']; } - fputcsv( $stream, $line ); // phpcs:ignore WordPressVIPMinimum.Functions.RestrictedFunctions.file_ops_fputcsv + $parsed[ 'input:' . $input['label'] ] = array( + 'label' => $input['label'], + 'value' => $value, + ); } - rewind( $stream ); - $csv = stream_get_contents( $stream ); - fclose( $stream ); + return $parsed; + } - // Prefix a UTF-8 BOM so Excel detects the encoding instead of mangling accented characters. - return "\xEF\xBB\xBF" . $csv; + /** + * Sanitize a cell value for CSV export, prefixing with a single quote if it looks like a formula. + * + * @param mixed $value Cell value. + * @return string + */ + private function sanitize_cell( $value ) { + $value = strval( $value ); + + return preg_match( '/^[\x00-\x20]*[=+\-@]/', $value ) ? "'" . $value : $value; } } diff --git a/tests/test-form-submissions.php b/tests/test-form-submissions.php index 1629833e9..ede6a7c3a 100644 --- a/tests/test-form-submissions.php +++ b/tests/test-form-submissions.php @@ -895,6 +895,64 @@ public function test_export_csv_contains_submission_data() { $this->assertStringContainsString( 'Analytical Engines Ltd', $output ); } + /** + * Ensure the CSV export walks past the first batch: every submission gets a row, and a field + * that only shows up in a later batch still makes it into the header row. + */ + public function test_export_csv_paginates_beyond_a_single_batch() { + if ( ! \ThemeIsle\GutenbergBlocks\Pro::is_pro_installed() ) { + $this->markTestSkipped( 'Otter Pro is not installed in this test environment.' ); + } + + add_filter( 'product_otter_license_status', array( $this, 'valid_license' ) ); + + $total = Form_Records_Export::EXPORT_BATCH_SIZE + 5; + + for ( $index = 0; $index < $total; $index++ ) { + // Only the very last submission carries the late field, so it lands in a second batch. + $label = $index === $total - 1 ? 'Late Field' : 'Name'; + + $this->create_record( + 'unread', + array( + 'inputs' => array( + 'input01' => array( + 'label' => $label, + 'value' => 'Submission ' . $index, + 'type' => 'text', + ), + ), + ) + ); + } + + wp_set_current_user( $this->admin_id ); + $_POST['_nonce'] = wp_create_nonce( 'otter_form_export_submissions' ); + $_POST['format'] = 'csv'; + + ob_start(); + + try { + ( new Form_Records_Export() )->export_submissions(); + } catch ( WPDieException $exception ) { + // export_submissions() always ends in wp_die(); assertions run on the buffered output below. + } + + $output = ob_get_clean(); + + $lines = array_filter( explode( "\n", trim( $output ) ) ); + + // One header row plus one row per submission. + $this->assertCount( $total + 1, $lines ); + + // The header is built from a full pass, so a label from a later batch is not missed. + $this->assertStringContainsString( 'Late Field', reset( $lines ) ); + + $this->assertStringContainsString( 'Submission 0', $output ); + $this->assertStringContainsString( 'Submission ' . ( Form_Records_Export::EXPORT_BATCH_SIZE - 1 ), $output ); + $this->assertStringContainsString( 'Submission ' . ( $total - 1 ), $output ); + } + /** * Ensure the list-table bulk actions match the current status view. */ From e632e8e817f4afbb4553b8eb5176626786ac882a Mon Sep 17 00:00:00 2001 From: girishpanchal30 Date: Wed, 29 Jul 2026 11:54:16 +0530 Subject: [PATCH 05/10] fix: enhance CSV export with max submission ID handling --- inc/plugins/class-form-records-export.php | 102 ++++++++++++++++------ 1 file changed, 75 insertions(+), 27 deletions(-) diff --git a/inc/plugins/class-form-records-export.php b/inc/plugins/class-form-records-export.php index c02f38460..59f249ca1 100644 --- a/inc/plugins/class-form-records-export.php +++ b/inc/plugins/class-form-records-export.php @@ -23,6 +23,13 @@ class Form_Records_Export { */ const EXPORT_BATCH_SIZE = 100; + /** + * Post statuses. + * + * @var string[] + */ + const EXPORT_POST_STATUSES = array( 'draft', 'unread', 'read', 'trash', 'publish' ); + /** * Register hooks. * @@ -76,12 +83,14 @@ private function export_xml() { } /** - * Build a CSV export of all submissions. + * Write a CSV export of all submissions to the output. * * @return void */ private function export_csv() { - $columns = array_merge( $this->get_fixed_columns(), $this->collect_input_columns() ); + $max_id = (int) $this->get_max_submission_id(); + + $columns = array_merge( $this->get_fixed_columns(), $this->collect_input_columns( $max_id ) ); $stream = fopen( 'php://output', 'w' ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fopen @@ -97,8 +106,9 @@ private function export_csv() { $column_keys = array_keys( $columns ); $this->walk_submissions( - function ( $meta, $record ) use ( $stream, $column_keys ) { - $row = $this->get_record_row( $record, $meta ); + $max_id, + function ( $meta, $post_id ) use ( $stream, $column_keys ) { + $row = $this->get_record_row( $post_id, $meta ); $line = array(); foreach ( $column_keys as $key ) { @@ -130,12 +140,14 @@ private function get_fixed_columns() { /** * Collect the dynamically generated input columns found across every submission. * + * @param int $max_id Highest submission ID to export. * @return array Column key => header label, in order of first appearance. */ - private function collect_input_columns() { + private function collect_input_columns( $max_id ) { $input_columns = array(); $this->walk_submissions( + $max_id, function ( $meta ) use ( &$input_columns ) { foreach ( $this->get_record_inputs( $meta ) as $column_key => $input ) { if ( isset( $input_columns[ $column_key ] ) ) { @@ -153,22 +165,24 @@ function ( $meta ) use ( &$input_columns ) { /** * Run a callback over every submission, loading them in bounded batches. * - * @param callable $callback Receives the submission record meta and the submission post. + * @param int $max_id Highest submission ID to export. + * @param callable $callback Receives the submission record meta and the submission post ID. * @return void */ - private function walk_submissions( $callback ) { - $offset = 0; - $has_more_submissions = false; + private function walk_submissions( $max_id, $callback ) { + $last_id = 0; do { - $records = $this->get_submissions_batch( $offset ); - $count = count( $records ); + $records = $this->get_submissions_batch( $last_id, $max_id ); - if ( 0 === $count ) { + // The batch is only limited, never offset, so an empty one means the range is done. + if ( empty( $records ) ) { return; } - $ids = wp_list_pluck( $records, 'ID' ); + $ids = wp_list_pluck( $records, 'ID' ); + $last_id = (int) end( $ids ); + update_meta_cache( 'post', $ids ); foreach ( $records as $record ) { @@ -178,9 +192,10 @@ private function walk_submissions( $callback ) { continue; } - $callback( $meta, $record ); + $callback( $meta, $record->ID ); } + // Drop the batch from the object cache, otherwise memory grows with every batch. foreach ( $ids as $id ) { wp_cache_delete( $id, 'posts' ); wp_cache_delete( $id, 'post_meta' ); @@ -188,42 +203,75 @@ private function walk_submissions( $callback ) { unset( $records, $ids ); - $offset += $count; - $has_more_submissions = self::EXPORT_BATCH_SIZE === $count; + $has_more_submissions = $last_id < $max_id; } while ( $has_more_submissions ); } /** - * Fetch a batch of submissions, oldest first. + * The highest submission ID eligible for export. * - * @param int $offset How many submissions to skip. + * @return int + */ + private function get_max_submission_id() { + $ids = get_posts( + array( + 'post_type' => Form_Submissions::FORM_RECORD_TYPE, + 'post_status' => self::EXPORT_POST_STATUSES, + 'posts_per_page' => 1, + 'orderby' => 'ID', + 'order' => 'DESC', + 'fields' => 'ids', + 'update_post_meta_cache' => false, + 'update_post_term_cache' => false, + ) + ); + + return empty( $ids ) ? 0 : reset( $ids ); + } + + /** + * Fetch the submissions following the last one handled, oldest first. + * + * @param int $last_id Highest submission ID handled so far. + * @param int $max_id Highest submission ID to export. * @return \WP_Post[] */ - private function get_submissions_batch( $offset ) { - return get_posts( + private function get_submissions_batch( $last_id, $max_id ) { + $keyset_clause = static function ( $where ) use ( $last_id, $max_id ) { + global $wpdb; + + return $where . $wpdb->prepare( " AND {$wpdb->posts}.ID > %d AND {$wpdb->posts}.ID <= %d", $last_id, $max_id ); + }; + + add_filter( 'posts_where', $keyset_clause ); + + $query = new \WP_Query( array( 'post_type' => Form_Submissions::FORM_RECORD_TYPE, - 'post_status' => array( 'draft', 'unread', 'read', 'trash', 'publish' ), + 'post_status' => self::EXPORT_POST_STATUSES, 'posts_per_page' => self::EXPORT_BATCH_SIZE, - 'offset' => $offset, 'orderby' => 'ID', 'order' => 'ASC', + 'no_found_rows' => true, 'update_post_meta_cache' => false, 'update_post_term_cache' => false, + 'suppress_filters' => false, ) ); + + remove_filter( 'posts_where', $keyset_clause ); + + return $query->posts; } /** * Build the CSV row of a single submission, keyed by column. * - * @param \WP_Post $record Submission post. - * @param array $meta Submission record meta. + * @param int $post_id Submission post ID. + * @param array $meta Submission record meta. * @return array */ - private function get_record_row( $record, $meta ) { - $post_id = $record->ID; - + private function get_record_row( $post_id, $meta ) { $row = array( 'id' => substr( strval( $post_id ), -8 ), 'status' => get_post_status( $post_id ), From 0bf12769265c56299bd4e9b4564af2c23804901d Mon Sep 17 00:00:00 2001 From: girishpanchal30 Date: Wed, 29 Jul 2026 12:29:38 +0530 Subject: [PATCH 06/10] fix: optimize query performance by disabling cache results --- inc/plugins/class-form-records-export.php | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/inc/plugins/class-form-records-export.php b/inc/plugins/class-form-records-export.php index 59f249ca1..87bb65504 100644 --- a/inc/plugins/class-form-records-export.php +++ b/inc/plugins/class-form-records-export.php @@ -221,6 +221,7 @@ private function get_max_submission_id() { 'orderby' => 'ID', 'order' => 'DESC', 'fields' => 'ids', + 'cache_results' => false, 'update_post_meta_cache' => false, 'update_post_term_cache' => false, ) @@ -243,7 +244,17 @@ private function get_submissions_batch( $last_id, $max_id ) { return $where . $wpdb->prepare( " AND {$wpdb->posts}.ID > %d AND {$wpdb->posts}.ID <= %d", $last_id, $max_id ); }; + /* + * A split query fetches the IDs and then primes the post cache with a second query, which + * is wasted work here: the batch is read once and its caches are dropped straight after. + * That priming is not covered by `cache_results`, so it has to be turned off separately. + */ + $single_query = static function () { + return false; + }; + add_filter( 'posts_where', $keyset_clause ); + add_filter( 'split_the_query', $single_query ); $query = new \WP_Query( array( @@ -253,12 +264,14 @@ private function get_submissions_batch( $last_id, $max_id ) { 'orderby' => 'ID', 'order' => 'ASC', 'no_found_rows' => true, + 'cache_results' => false, 'update_post_meta_cache' => false, 'update_post_term_cache' => false, 'suppress_filters' => false, ) ); + remove_filter( 'split_the_query', $single_query ); remove_filter( 'posts_where', $keyset_clause ); return $query->posts; From 5ebc94f708e161e7ed6e6b10e52df3abeb50e8f8 Mon Sep 17 00:00:00 2001 From: girishpanchal30 Date: Wed, 29 Jul 2026 13:16:18 +0530 Subject: [PATCH 07/10] fix: refactor CSV and XML export --- inc/plugins/class-form-records-export.php | 55 +++++++++++------------ 1 file changed, 25 insertions(+), 30 deletions(-) diff --git a/inc/plugins/class-form-records-export.php b/inc/plugins/class-form-records-export.php index 87bb65504..439c619d9 100644 --- a/inc/plugins/class-form-records-export.php +++ b/inc/plugins/class-form-records-export.php @@ -47,7 +47,7 @@ public function export_submissions() { wp_die( esc_html( __( 'Exporting submissions requires Otter Pro.', 'otter-blocks' ) ) ); } - $nonce = isset( $_POST['_nonce'] ) ? sanitize_text_field( wp_unslash( $_POST['_nonce'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended + $nonce = isset( $_POST['_nonce'] ) ? sanitize_text_field( wp_unslash( $_POST['_nonce'] ) ) : ''; if ( ! wp_verify_nonce( $nonce, 'otter_form_export_submissions' ) ) { wp_die( esc_html( __( 'Invalid nonce.', 'otter-blocks' ) ) ); } @@ -56,12 +56,14 @@ public function export_submissions() { wp_die( esc_html( __( 'You are not allowed to export submissions.', 'otter-blocks' ) ) ); } - $format = isset( $_POST['format'] ) ? sanitize_key( wp_unslash( $_POST['format'] ) ) : 'xml'; // phpcs:ignore WordPress.Security.NonceVerification.Recommended + $format = isset( $_POST['format'] ) ? sanitize_key( wp_unslash( $_POST['format'] ) ) : 'xml'; + + $output = new \SplFileObject( 'php://output', 'w' ); if ( 'csv' === $format ) { - $this->export_csv(); + $this->export_csv( $output ); } else { - echo $this->export_xml(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + $this->export_xml( $output ); } wp_die(); @@ -70,56 +72,50 @@ public function export_submissions() { /** * Build the WordPress WXR (XML) export of all submissions. * - * @return string + * @param \SplFileObject $output Output stream. + * @return void */ - private function export_xml() { + private function export_xml( $output ) { require_once ABSPATH . 'wp-admin/includes/export.php'; ob_start(); export_wp( array( 'content' => Form_Submissions::FORM_RECORD_TYPE ) ); $export = ob_get_clean(); - return ent2ncr( $export ); + $output->fwrite( ent2ncr( $export ) ); } /** * Write a CSV export of all submissions to the output. * + * @param \SplFileObject $output Output stream. * @return void */ - private function export_csv() { + private function export_csv( $output ) { $max_id = (int) $this->get_max_submission_id(); $columns = array_merge( $this->get_fixed_columns(), $this->collect_input_columns( $max_id ) ); - $stream = fopen( 'php://output', 'w' ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fopen - - if ( false === $stream ) { - return; - } - // Prefix a UTF-8 BOM so Excel detects the encoding instead of mangling accented characters. - fwrite( $stream, "\xEF\xBB\xBF" ); // phpcs:ignore WordPressVIPMinimum.Functions.RestrictedFunctions.file_ops_fwrite + $output->fwrite( "\xEF\xBB\xBF" ); - fputcsv( $stream, array_map( array( $this, 'sanitize_cell' ), array_values( $columns ) ) ); // phpcs:ignore WordPressVIPMinimum.Functions.RestrictedFunctions.file_ops_fputcsv + $output->fputcsv( array_map( array( $this, 'sanitize_cell' ), array_values( $columns ) ) ); $column_keys = array_keys( $columns ); $this->walk_submissions( $max_id, - function ( $meta, $post_id ) use ( $stream, $column_keys ) { - $row = $this->get_record_row( $post_id, $meta ); + function ( $meta, $record ) use ( $output, $column_keys ) { + $row = $this->get_record_row( $record, $meta ); $line = array(); foreach ( $column_keys as $key ) { $line[] = $this->sanitize_cell( isset( $row[ $key ] ) ? $row[ $key ] : '' ); } - fputcsv( $stream, $line ); // phpcs:ignore WordPressVIPMinimum.Functions.RestrictedFunctions.file_ops_fputcsv + $output->fputcsv( $line ); } ); - - fclose( $stream ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose } /** @@ -166,7 +162,7 @@ function ( $meta ) use ( &$input_columns ) { * Run a callback over every submission, loading them in bounded batches. * * @param int $max_id Highest submission ID to export. - * @param callable $callback Receives the submission record meta and the submission post ID. + * @param callable $callback Receives the submission record meta and the submission post. * @return void */ private function walk_submissions( $max_id, $callback ) { @@ -192,12 +188,11 @@ private function walk_submissions( $max_id, $callback ) { continue; } - $callback( $meta, $record->ID ); + $callback( $meta, $record ); } // Drop the batch from the object cache, otherwise memory grows with every batch. foreach ( $ids as $id ) { - wp_cache_delete( $id, 'posts' ); wp_cache_delete( $id, 'post_meta' ); } @@ -280,15 +275,15 @@ private function get_submissions_batch( $last_id, $max_id ) { /** * Build the CSV row of a single submission, keyed by column. * - * @param int $post_id Submission post ID. - * @param array $meta Submission record meta. + * @param \WP_Post $record Submission post. + * @param array $meta Submission record meta. * @return array */ - private function get_record_row( $post_id, $meta ) { + private function get_record_row( $record, $meta ) { $row = array( - 'id' => substr( strval( $post_id ), -8 ), - 'status' => get_post_status( $post_id ), - 'date' => get_the_date( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ), $post_id ), + 'id' => substr( strval( $record->ID ), -8 ), + 'status' => get_post_status( $record ), + 'date' => get_the_date( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ), $record ), 'form' => isset( $meta['form']['value'] ) ? $meta['form']['value'] : '', 'post' => isset( $meta['post_url']['value'] ) ? $meta['post_url']['value'] : '', ); From cb1d14f30039b615f52eee6c9de760b4728175c7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 30 Jul 2026 13:45:22 +0000 Subject: [PATCH 08/10] build(deps): bump codeinwp/themeisle-sdk from 3.3.54 to 3.3.58 Bumps [codeinwp/themeisle-sdk](https://github.com/Codeinwp/themeisle-sdk) from 3.3.54 to 3.3.58. - [Release notes](https://github.com/Codeinwp/themeisle-sdk/releases) - [Changelog](https://github.com/Codeinwp/themeisle-sdk/blob/v3.3.58/CHANGELOG.md) - [Commits](https://github.com/Codeinwp/themeisle-sdk/compare/v3.3.54...v3.3.58) --- updated-dependencies: - dependency-name: codeinwp/themeisle-sdk dependency-version: 3.3.58 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- composer.lock | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/composer.lock b/composer.lock index 0d8f0c84d..c3be5643a 100644 --- a/composer.lock +++ b/composer.lock @@ -4,20 +4,20 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "b41cfd1bb81a9d4f8ed32b2381a22d65", + "content-hash": "556f279ce85f25f708f60329f1f7067c", "packages": [ { "name": "codeinwp/themeisle-sdk", - "version": "3.3.54", + "version": "3.3.58", "source": { "type": "git", "url": "https://github.com/Codeinwp/themeisle-sdk.git", - "reference": "095c2d0f1388af0b0196c492a7f79e2fd092dab1" + "reference": "d6807c0b7308e323bd77cced667dee3f2d5e6a82" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Codeinwp/themeisle-sdk/zipball/095c2d0f1388af0b0196c492a7f79e2fd092dab1", - "reference": "095c2d0f1388af0b0196c492a7f79e2fd092dab1", + "url": "https://api.github.com/repos/Codeinwp/themeisle-sdk/zipball/d6807c0b7308e323bd77cced667dee3f2d5e6a82", + "reference": "d6807c0b7308e323bd77cced667dee3f2d5e6a82", "shasum": "" }, "require-dev": { @@ -36,16 +36,16 @@ "homepage": "https://themeisle.com" } ], - "description": "Themeisle SDK.", + "description": "Themeisle SDK library.", "homepage": "https://github.com/Codeinwp/themeisle-sdk", "keywords": [ "wordpress" ], "support": { "issues": "https://github.com/Codeinwp/themeisle-sdk/issues", - "source": "https://github.com/Codeinwp/themeisle-sdk/tree/v3.3.54" + "source": "https://github.com/Codeinwp/themeisle-sdk/tree/v3.3.58" }, - "time": "2026-06-23T13:43:47+00:00" + "time": "2026-07-29T08:38:52+00:00" }, { "name": "enshrined/svg-sanitize", From 5e42f8e47b793c0e9e4f1b83628c5d5c47558d54 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 1 Aug 2026 23:45:21 +0000 Subject: [PATCH 09/10] build(deps): bump codeinwp/themeisle-sdk in /plugins/blocks-css Bumps [codeinwp/themeisle-sdk](https://github.com/Codeinwp/themeisle-sdk) from 3.3.54 to 3.3.58. - [Release notes](https://github.com/Codeinwp/themeisle-sdk/releases) - [Changelog](https://github.com/Codeinwp/themeisle-sdk/blob/v3.3.58/CHANGELOG.md) - [Commits](https://github.com/Codeinwp/themeisle-sdk/compare/v3.3.54...v3.3.58) --- updated-dependencies: - dependency-name: codeinwp/themeisle-sdk dependency-version: 3.3.58 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- plugins/blocks-css/composer.lock | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/plugins/blocks-css/composer.lock b/plugins/blocks-css/composer.lock index 215845bb9..c2a3c2df6 100644 --- a/plugins/blocks-css/composer.lock +++ b/plugins/blocks-css/composer.lock @@ -8,16 +8,16 @@ "packages": [ { "name": "codeinwp/themeisle-sdk", - "version": "3.3.54", + "version": "3.3.58", "source": { "type": "git", "url": "https://github.com/Codeinwp/themeisle-sdk.git", - "reference": "095c2d0f1388af0b0196c492a7f79e2fd092dab1" + "reference": "d6807c0b7308e323bd77cced667dee3f2d5e6a82" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Codeinwp/themeisle-sdk/zipball/095c2d0f1388af0b0196c492a7f79e2fd092dab1", - "reference": "095c2d0f1388af0b0196c492a7f79e2fd092dab1", + "url": "https://api.github.com/repos/Codeinwp/themeisle-sdk/zipball/d6807c0b7308e323bd77cced667dee3f2d5e6a82", + "reference": "d6807c0b7308e323bd77cced667dee3f2d5e6a82", "shasum": "" }, "require-dev": { @@ -36,16 +36,16 @@ "homepage": "https://themeisle.com" } ], - "description": "Themeisle SDK.", + "description": "Themeisle SDK library.", "homepage": "https://github.com/Codeinwp/themeisle-sdk", "keywords": [ "wordpress" ], "support": { "issues": "https://github.com/Codeinwp/themeisle-sdk/issues", - "source": "https://github.com/Codeinwp/themeisle-sdk/tree/v3.3.54" + "source": "https://github.com/Codeinwp/themeisle-sdk/tree/v3.3.58" }, - "time": "2026-06-23T13:43:47+00:00" + "time": "2026-07-29T08:38:52+00:00" } ], "packages-dev": [], From d07ea2cab88a44d2df1525b492d7f34f669f18f7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 1 Aug 2026 23:45:23 +0000 Subject: [PATCH 10/10] build(deps): bump codeinwp/themeisle-sdk in /plugins/blocks-animation Bumps [codeinwp/themeisle-sdk](https://github.com/Codeinwp/themeisle-sdk) from 3.3.54 to 3.3.58. - [Release notes](https://github.com/Codeinwp/themeisle-sdk/releases) - [Changelog](https://github.com/Codeinwp/themeisle-sdk/blob/v3.3.58/CHANGELOG.md) - [Commits](https://github.com/Codeinwp/themeisle-sdk/compare/v3.3.54...v3.3.58) --- updated-dependencies: - dependency-name: codeinwp/themeisle-sdk dependency-version: 3.3.58 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- plugins/blocks-animation/composer.lock | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/plugins/blocks-animation/composer.lock b/plugins/blocks-animation/composer.lock index b11e5749e..d03d95990 100644 --- a/plugins/blocks-animation/composer.lock +++ b/plugins/blocks-animation/composer.lock @@ -8,16 +8,16 @@ "packages": [ { "name": "codeinwp/themeisle-sdk", - "version": "3.3.54", + "version": "3.3.58", "source": { "type": "git", "url": "https://github.com/Codeinwp/themeisle-sdk.git", - "reference": "095c2d0f1388af0b0196c492a7f79e2fd092dab1" + "reference": "d6807c0b7308e323bd77cced667dee3f2d5e6a82" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Codeinwp/themeisle-sdk/zipball/095c2d0f1388af0b0196c492a7f79e2fd092dab1", - "reference": "095c2d0f1388af0b0196c492a7f79e2fd092dab1", + "url": "https://api.github.com/repos/Codeinwp/themeisle-sdk/zipball/d6807c0b7308e323bd77cced667dee3f2d5e6a82", + "reference": "d6807c0b7308e323bd77cced667dee3f2d5e6a82", "shasum": "" }, "require-dev": { @@ -36,16 +36,16 @@ "homepage": "https://themeisle.com" } ], - "description": "Themeisle SDK.", + "description": "Themeisle SDK library.", "homepage": "https://github.com/Codeinwp/themeisle-sdk", "keywords": [ "wordpress" ], "support": { "issues": "https://github.com/Codeinwp/themeisle-sdk/issues", - "source": "https://github.com/Codeinwp/themeisle-sdk/tree/v3.3.54" + "source": "https://github.com/Codeinwp/themeisle-sdk/tree/v3.3.58" }, - "time": "2026-06-23T13:43:47+00:00" + "time": "2026-07-29T08:38:52+00:00" } ], "packages-dev": [],