Skip to content

Add CSV download through AJAX - #449

Merged
sorinmarta merged 9 commits into
masterfrom
fix-317
Aug 13, 2025
Merged

Add CSV download through AJAX#449
sorinmarta merged 9 commits into
masterfrom
fix-317

Conversation

@sorinmarta

@sorinmarta sorinmarta commented Aug 13, 2025

Copy link
Copy Markdown
Contributor

fixes https://github.com/Strategy11/business-directory-premium/issues/317

This PR forces download of CSV files on export by forcing the right headers on the download link. This is needed because some hosting providers add the Content-Type: text/csv to the files so the browser open the contents as text.

We do this without using the WordPress filesystem because we will have large export files that need to be streamed and they should not be loaded in memory.

@sorinmarta sorinmarta added the run analysis Runs phpcs and phpunit label Aug 13, 2025
@coderabbitai

coderabbitai Bot commented Aug 13, 2025

Copy link
Copy Markdown

Caution

Review failed

The pull request is closed.

Walkthrough

Adds a signed AJAX download endpoint for CSV exports and returns its URL in export responses; admin JS prefers that URL and trims whitespace before use. Also expands PHP-CS-Fixer Finder exclusions to ignore node_modules in addition to vendors.

Changes

Cohort / File(s) Summary of changes
Tooling config
./.php-cs-fixer.php
Finder exclusions expanded from vendors to also exclude node_modules by adding ->exclude('node_modules').
Admin export frontend
assets/js/admin-export.js
In finalization path, computes `const downloadUrl = (res.download_url
Admin CSV export backend
includes/admin/csv-export.php
Adds wpbdp-csv-download AJAX handler; adds public function ajax_csv_download() to stream files with permission and nonce checks, state decoding, file existence validation, streaming headers, and readfile output; adds get_download_url($state) to generate a signed admin-ajax URL; extends ajax_csv_export() responses to include download_url when export is done.

Sequence Diagram(s)

sequenceDiagram
  participant U as Admin User
  participant UI as Admin UI (JS)
  participant AE as AJAX Export Endpoint
  participant AD as AJAX Download Endpoint
  participant FS as Filesystem

  U->>UI: Start CSV export
  UI->>AE: ajax_csv_export(state)
  AE-->>UI: Progress updates
  AE-->>UI: Final response with download_url (or fileurl)
  UI-->>U: Show "Download" link (uses trimmed download_url/fileurl)
  U->>UI: Click download link
  UI->>AD: ajax_csv_download(state + nonce)
  AD->>FS: Locate and read file
  FS-->>AD: File contents
  AD-->>UI: Stream file (headers + bytes)
  UI-->>U: Browser downloads CSV
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Suggested reviewers

  • shervElmi
  • garretlaxton

📜 Recent review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8785e48 and 55c1d1f.

📒 Files selected for processing (2)
  • assets/js/admin-export.js (1 hunks)
  • includes/admin/csv-export.php (2 hunks)
✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix-317

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (3)
includes/admin/csv-export.php (3)

86-92: Consider updating the version placeholder in the docblock.

The @since tag currently shows x.x which should be replaced with the actual version number before merging.


140-147: Consider using readfile() or chunked reading for large files.

While using $wp_filesystem->get_contents() works, it loads the entire file into memory. For large CSV exports, this could cause memory issues. Consider using readfile() or chunked reading for better memory efficiency.

-$file_content = $wp_filesystem->get_contents( $file_path );
-
-if ( ! $file_content ) {
-	wp_die( esc_html__( 'Could not read export file.', 'business-directory-plugin' ) );
-}
-
-echo $file_content;
+// Use readfile for better memory efficiency with large files
+$result = @readfile( $file_path );
+
+if ( false === $result ) {
+	wp_die( esc_html__( 'Could not read export file.', 'business-directory-plugin' ) );
+}

Alternatively, for even better control and compatibility with WordPress Filesystem:

-$file_content = $wp_filesystem->get_contents( $file_path );
-
-if ( ! $file_content ) {
-	wp_die( esc_html__( 'Could not read export file.', 'business-directory-plugin' ) );
-}
-
-echo $file_content;
+// Read and output file in chunks for better memory efficiency
+$handle = @fopen( $file_path, 'rb' );
+
+if ( ! $handle ) {
+	wp_die( esc_html__( 'Could not read export file.', 'business-directory-plugin' ) );
+}
+
+while ( ! feof( $handle ) ) {
+	echo fread( $handle, 8192 ); // 8KB chunks
+	flush();
+}
+
+fclose( $handle );

155-162: Consider updating the version placeholder in the docblock.

Similar to the earlier comment, the @since tag shows x.x which should be replaced with the actual version number.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ad14fd0 and f297663.

⛔ Files ignored due to path filters (3)
  • assets/js/admin-csv-import.min.js is excluded by !**/*.min.js, !**/*.min.js
  • assets/js/admin-export.min.js is excluded by !**/*.min.js, !**/*.min.js
  • assets/js/onboarding-wizard.min.js is excluded by !**/*.min.js, !**/*.min.js
📒 Files selected for processing (3)
  • .php-cs-fixer.php (1 hunks)
  • assets/js/admin-export.js (1 hunks)
  • includes/admin/csv-export.php (2 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (1)
includes/admin/csv-export.php (3)
includes/admin/helpers/csv/class-csv-exporter.php (3)
  • get_file_url (289-298)
  • from_state (151-174)
  • get_file_path (281-287)
includes/helpers/class-app.php (2)
  • WPBDP_App_Helper (9-573)
  • permission_check (212-231)
includes/utils.php (1)
  • wpbdp_get_var (570-591)
🔇 Additional comments (9)
.php-cs-fixer.php (1)

5-6: LGTM!

The addition of node_modules to the exclusion list is appropriate and follows standard practice for PHP projects that use npm packages.

includes/admin/csv-export.php (7)

15-15: LGTM! Well-implemented AJAX download handler registration.

Correctly registers the new AJAX action handler for CSV downloads, following WordPress conventions.


70-79: Good implementation of backward-compatible download URL.

The response structure correctly includes both the new download_url field and maintains the existing fileurl field for backward compatibility. The conditional generation of the download URL only when the export is done is appropriate.


93-108: Solid permission and validation checks.

The method properly validates permissions, verifies the nonce, and validates the state parameter before processing. The error messages are clear and properly escaped.


114-119: Good use of WordPress Filesystem API.

Properly initializes the WordPress Filesystem API when needed, which is the recommended approach for file operations in WordPress plugins.


128-134: Excellent header implementation to force file download.

The headers correctly:

  1. Override problematic text/csv headers with application/octet-stream
  2. Force download with Content-Disposition: attachment
  3. Include file size for download progress
  4. Prevent caching to ensure fresh downloads

This directly addresses the PR's objective of forcing CSV downloads instead of displaying in the browser.


136-138: Proper output buffer cleanup.

The loop ensures all output buffers are cleared before sending file content, preventing any unwanted output from corrupting the download.


163-172: Well-structured download URL generation.

The method correctly creates a signed admin-ajax URL with all necessary parameters for secure file download. The use of wp_create_nonce() ensures the download link is secure and can't be easily forged.

assets/js/admin-export.js (1)

86-89: Clean implementation with proper fallback support.

The code elegantly handles both the new AJAX download endpoint and maintains backward compatibility by falling back to the direct file URL when download_url is not available. This ensures the feature works correctly with both old and new server responses.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (2)
includes/admin/csv-export.php (2)

86-92: Update the @SInCE version tag.

The @since x.x placeholder should be replaced with the actual version number for this release.

- * @since x.x
+ * @since 6.4.3

162-169: Update the @SInCE version tag.

The @since x.x placeholder should be replaced with the actual version number for this release.

- * @since x.x
+ * @since 6.4.3
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f297663 and 8785e48.

📒 Files selected for processing (1)
  • includes/admin/csv-export.php (2 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (1)
includes/admin/csv-export.php (4)
assets/js/onboarding-wizard/utils/url.js (1)
  • state (44-44)
includes/admin/helpers/csv/class-csv-exporter.php (5)
  • get_file_url (289-298)
  • WPBDP_CSVExporter (13-496)
  • from_state (151-174)
  • get_file_path (281-287)
  • header (300-315)
includes/helpers/class-app.php (1)
  • permission_check (212-231)
includes/utils.php (1)
  • wpbdp_get_var (570-591)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: PHPStan
🔇 Additional comments (10)
includes/admin/csv-export.php (10)

15-15: LGTM: AJAX handler registration follows WordPress conventions.

The new AJAX action is properly registered using WordPress hooks with the expected naming pattern.


70-79: LGTM: Response structure enhanced with download URL.

The response array has been properly restructured and includes the new download_url field that will be used by the frontend JavaScript to trigger forced downloads.


93-96: LGTM: Proper security checks implemented.

The function correctly implements WordPress security best practices with permission checking and nonce verification.


97-107: LGTM: State validation with appropriate error handling.

The state parameter validation is thorough and uses WordPress's wp_die() function for proper error termination in AJAX contexts.


109-116: LGTM: Export reconstruction and file validation.

The code properly reconstructs the export object from state and validates file existence before proceeding with the download.


118-127: LGTM: Headers correctly configured to force download.

The headers are well-configured to solve the original problem:

  • application/octet-stream overrides hosting provider's text/csv headers
  • Content-Disposition: attachment forces download instead of browser display
  • Cache control headers prevent caching issues

129-131: LGTM: Output buffer cleanup before file streaming.

Properly clears all output buffers to prevent interference with the file download stream.


134-151: LGTM: Efficient file streaming implementation.

The chunked file reading approach is memory-efficient and includes proper error handling. The 8KB chunk size is appropriate for balancing memory usage and performance.


153-160: LGTM: Proper resource cleanup and error handling.

File handle is properly closed and exceptions are handled with wp_die() for consistent error reporting.


171-180: LGTM: URL generation follows WordPress conventions.

The download URL is properly constructed using add_query_arg() and includes all necessary parameters with proper nonce generation.

@sorinmarta
sorinmarta requested a review from shervElmi August 13, 2025 09:56

@shervElmi shervElmi left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!

Thanks @sorinmarta.

Comment thread assets/js/admin-export.js Outdated
Comment thread includes/admin/csv-export.php
@sorinmarta

Copy link
Copy Markdown
Contributor Author

Thanks @shervElmi!

@sorinmarta
sorinmarta merged commit e4deace into master Aug 13, 2025
4 checks passed
@sorinmarta
sorinmarta deleted the fix-317 branch August 13, 2025 12:10
@coderabbitai coderabbitai Bot mentioned this pull request Oct 27, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

run analysis Runs phpcs and phpunit

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants