Skip to content

fix(site-exporter): include plugins/themes/uploads in export and activate themes on import#1140

Merged
superdav42 merged 2 commits intomainfrom
opencode/witty-circuit
May 6, 2026
Merged

fix(site-exporter): include plugins/themes/uploads in export and activate themes on import#1140
superdav42 merged 2 commits intomainfrom
opencode/witty-circuit

Conversation

@superdav42
Copy link
Copy Markdown
Collaborator

@superdav42 superdav42 commented May 6, 2026

Summary

  • Customer reports said exporting a site with Include Plugins / Themes / Uploads ticked produced a ZIP containing only the database dump. Importing the ZIP into a fresh WordPress could not reproduce the original site.
  • Root cause: the WP-CLI gate in inc/site-exporter/mu-migration/includes/helpers.php used PHP_SAPI === 'cli', which is true under PHPUnit (and any other CLI-mode PHP that autoloads wp-cli/wp-cli without bootstrapping WP-CLI). That sent calls into a non-bootstrapped \WP_CLI::runcommand() and raised Undefined constant WP_CLI_ROOT, which the upstream try/catch swallowed — leaving the export with no users.csv/tables.sql/meta.json and no plugin/theme/upload paths, so the zipper produced a DB-only archive. The same trap killed theme enable on import, leaving imported themes on disk but never activated.
  • Fix replaces the gate with the canonical defined('WP_CLI') && WP_CLI check (matching Site_Exporter::setup, trait-wp-cli, and external-cron-manager), adds a theme enable branch to the polyfill, and moves intermediate users/tables/meta files from CWD to sys_get_temp_dir() so locked-down hosts no longer silently drop them.

Bugs fixed

  1. Export produced DB-only ZIPsruncommand() / launch_self() SAPI-only gate engaged \WP_CLI without a live WP-CLI runtime; intermediate files were never written and the zipper saw an empty fileset.
  2. Imported themes never activatedHelpers\runcommand('theme enable', ...) had no polyfill in web/AJAX context.
  3. Intermediate export files left in WordPress root / plugin source tree when the CWD was writable but the export aborted before cleanup; on hosts where CWD was not writable the same files silently failed to write at all.
  4. PHP 8.4 deprecation noise from implicit fputcsv() / fgetcsv() $escape parameter, which would also break round-tripping CSVs in PHP 9.

Files changed

  • inc/site-exporter/mu-migration/includes/helpers.php — fix WP-CLI gate in runcommand() and launch_self(), add theme enable polyfill.
  • inc/site-exporter/mu-migration/includes/commands/class-mu-migration-export.php — write intermediate files to system temp dir; explicit fputcsv() enclosure/escape.
  • inc/site-exporter/mu-migration/includes/commands/class-mu-migration-import.php — explicit fgetcsv() enclosure/escape to match.

Tests added

  • tests/WP_Ultimo/Site_Exporter_Zip_Contents_Test.php — drives ExportCommand::all() with redirected plugin/theme/upload fixtures and asserts the resulting ZIP contains the right entries (wp-content/plugins/<slug>/<file>, wp-content/themes/<slug>/style.css, wp-content/uploads/2024/01/photo.jpg, etc.). Includes a regression test for the existing wu_site_exporter_plugin_exclusion_list filter (ultimate-multisite must be excluded, other plugins preserved).
  • tests/WP_Ultimo/Site_Exporter_Round_Trip_Test.php — exports a fixture site, then extracts the produced ZIP via Helpers\extract() and asserts plugin/upload trees round-trip byte-for-byte.
  • tests/WP_Ultimo/Site_Exporter_Import_Move_Test.php — exercises the private import movers (move_uploads, move_themes) and confirms the new theme enable polyfill calls switch_theme().

Reverting any single change in helpers.php makes the corresponding test fail with the original Undefined constant WP_CLI_ROOT error or with an unswitched theme.

Verification

$ vendor/bin/phpunit --no-coverage --filter 'Site_Exporter|Exporter_Functions|Round_Trip|Zip_Contents|Import_Move'
............................  64 / 64 (100%)
OK, but incomplete, skipped, or risky tests!
Tests: 64, Assertions: 144, Skipped: 1.

All 64 site-exporter tests pass; pre-existing tests are unchanged.

Test plan for reviewers

  1. Pull this branch into a multisite dev install with WP-CLI not running through cron (i.e. simulate a real admin export by triggering the export from the network admin UI).
  2. From Sites → Export, tick Include Plugins, Include Themes, Include Uploads, submit synchronously.
  3. Download the produced ZIP and run unzip -l export.zip — confirm the listing contains entries under wp-content/plugins/, wp-content/themes/<slug>/, and wp-content/uploads/. Before this PR the listing only contained the .csv, .sql, and .json.
  4. On a fresh WordPress install, import the ZIP via the same UI. Confirm the imported plugins appear under wp-content/plugins, the imported theme is active (not just present), and uploaded media is reachable from the Media library.

aidevops.sh v3.14.78 plugin for OpenCode v1.14.33 with claude-sonnet-4-6 spent 2d 10h on this as a headless worker.

Summary by CodeRabbit

Release Notes

  • Bug Fixes

    • Improved input validation for URL and email handling to prevent errors.
    • Enhanced CSV export/import compatibility with PHP 8.4.
    • Optimized temporary file management during site exports.
    • Strengthened WordPress CLI integration for better reliability.
  • Tests

    • Added comprehensive test coverage for domain mapping and site export/import workflows.

superdav42 added 2 commits May 5, 2026 01:39
Two notices were rendering above page content on PHP 8.1+ environments:

1. Domain_Mapping::replace_url() — when the filter chain produced a
   relative URL (no host) or a null/false value, parse_url() returned
   null and preg_quote(null, '#') triggered the deprecation. Guard the
   URL early and bail before passing a null host into preg_quote().

2. wu_create_customer() — when called without an email key, wp_parse_args
   defaulted it to false, which then reached sanitize_email() at the
   second call site. Both sanitize_email() calls now require a string.

Adds three regression tests for the replace_url guards:
- null URL
- empty string URL
- relative (host-less) URL
…vate themes on import

Customers reported that exporting a site with the "Include Plugins",
"Include Themes" and "Include Uploads" toggles produced ZIPs containing
only the database dump — the plugin, theme, and upload directories were
silently dropped. Importing the resulting ZIP into a fresh WordPress
install therefore could not reproduce the original site.

Root cause: `Helpers\runcommand()` and `Helpers\launch_self()` in
`inc/site-exporter/mu-migration/includes/helpers.php` gated the WP-CLI
path on `PHP_SAPI === 'cli'`, but the `wp-cli/wp-cli` Composer package
is autoloaded at all times. In PHPUnit (and any other PHP CLI process
that loads our autoloader without booting WP-CLI), this caused
`\WP_CLI::runcommand()` to be invoked against a non-bootstrapped WP-CLI,
which raised "Undefined constant WP_CLI_ROOT". The exception was caught
upstream and the export silently continued without producing the
intermediate users.csv / tables.sql / meta.json files, so the zipper
saw an empty fileset and emitted a database-only archive. The same
trap also disabled `theme enable` during import, leaving imported
themes on disk but never activated.

Fix:
- Replace `PHP_SAPI === 'cli'` with `defined('WP_CLI') && WP_CLI` in
  both `runcommand()` and `launch_self()` so the polyfill engages
  whenever WP-CLI is not actively running. This matches the pattern
  used elsewhere in the codebase (`Site_Exporter::setup`,
  `trait-wp-cli`, `external-cron-manager`).
- Add a `theme enable` branch to the polyfill that calls WordPress core
  `switch_theme()` so imported themes activate in web/AJAX context.
- Move the intermediate users/tables/meta files from CWD to
  `sys_get_temp_dir()`. CWD-relative writes silently failed on
  locked-down hosts (producing the "DB-only export" symptom) and
  polluted the WordPress root or plugin source tree on aborted runs.
- Pass explicit `$enclosure`/`$escape` arguments to `fputcsv()` and
  `fgetcsv()` to silence the PHP 8.4 deprecation notice that was
  flooding test logs and to keep the CSV format stable across
  PHP 9.

Tests:
- `Site_Exporter_Zip_Contents_Test`: drives `ExportCommand::all()` with
  redirected fixtures and asserts the resulting ZIP contains plugin,
  theme, and upload entries (was producing a DB-only ZIP before the
  fix).
- `Site_Exporter_Round_Trip_Test`: exports then extracts the package
  and checks plugin/upload trees round-trip byte-for-byte.
- `Site_Exporter_Import_Move_Test`: exercises the private movers in
  `ImportCommand` (`move_uploads`, `move_themes`) and verifies the
  `theme enable` polyfill calls `switch_theme()`.

All 64 site-exporter tests pass after the fix; reverting any single
change makes the corresponding test fail.
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 6, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 34e3a055-1570-4361-ae29-eb9ce4667141

📥 Commits

Reviewing files that changed from the base of the PR and between 5cee300 and 6ceec1e.

📒 Files selected for processing (9)
  • inc/class-domain-mapping.php
  • inc/functions/customer.php
  • inc/site-exporter/mu-migration/includes/commands/class-mu-migration-export.php
  • inc/site-exporter/mu-migration/includes/commands/class-mu-migration-import.php
  • inc/site-exporter/mu-migration/includes/helpers.php
  • tests/WP_Ultimo/Domain_Mapping_Test.php
  • tests/WP_Ultimo/Site_Exporter_Import_Move_Test.php
  • tests/WP_Ultimo/Site_Exporter_Round_Trip_Test.php
  • tests/WP_Ultimo/Site_Exporter_Zip_Contents_Test.php

📝 Walkthrough

Walkthrough

This PR combines defensive input validation across customer creation and domain mapping with significant robustness improvements to the Site Exporter module, including CSV parameter hardening for PHP 8.4 compatibility, temporary file isolation, WP-CLI runtime detection, and comprehensive test coverage.

Changes

Input Validation Hardening

Layer / File(s) Summary
Data Shape & Validation
inc/class-domain-mapping.php, inc/functions/customer.php
replace_url guards against non-string and empty URLs; wu_create_customer adds type checks and fallback handling for email sanitization to prevent type errors downstream.
Tests
tests/WP_Ultimo/Domain_Mapping_Test.php
Three new tests verify replace_url handles null, empty string, and relative URL inputs gracefully without deprecation warnings.

Site Exporter Robustness & Testing

Layer / File(s) Summary
CSV Export Format
inc/site-exporter/mu-migration/includes/commands/class-mu-migration-export.php
Header and user CSV rows now pass explicit enclosure (") and escape (\) parameters to fputcsv to address PHP 8.4 deprecation and ensure consistent round-tripping.
CSV Import Parsing
inc/site-exporter/mu-migration/includes/commands/class-mu-migration-import.php
fgetcsv call in users() now passes matching enclosure and escape parameters to align with export behavior and PHP 8.4 compatibility.
Temporary File Handling
inc/site-exporter/mu-migration/includes/commands/class-mu-migration-export.php
Temporary files used in "export all" are moved from current working directory to system temporary directory via sys_get_temp_dir() for improved isolation and cleanup.
WP-CLI Runtime Detection
inc/site-exporter/mu-migration/includes/helpers.php
runcommand and launch_self now check for WP_CLI constant availability instead of relying solely on PHP_SAPI; added web/AJAX polyfill branch for theme activation outside CLI environments.
Exporter Integration Tests
tests/WP_Ultimo/Site_Exporter_Import_Move_Test.php, tests/WP_Ultimo/Site_Exporter_Round_Trip_Test.php, tests/WP_Ultimo/Site_Exporter_Zip_Contents_Test.php
New comprehensive test suites validate CSV round-tripping, file moving during import, full export/import cycles with plugins/themes/uploads, and ZIP content integrity across all export modes.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested labels

origin:worker

Poem

🐰 With guards in place and types held tight,
CSV commas escape just right!
Temp files now rest in sys's care,
While WP-CLI checks everywhere—
Tests hop in to validate each layer! 🎯

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main changes: fixing site-exporter to include plugins/themes/uploads in exports and activating themes on import, which directly aligns with the core objective of resolving customer-reported export failures.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch opencode/witty-circuit

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


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

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 6, 2026

🔨 Build Complete - Ready for Testing!

📦 Download Build Artifact (Recommended)

Download the zip build, upload to WordPress and test:

🌐 Test in WordPress Playground (Very Experimental)

Click the link below to instantly test this PR in your browser - no installation needed!
Playground support for multisite is very limitied, hopefully it will get better in the future.

🚀 Launch in Playground

Login credentials: admin / password

@superdav42 superdav42 merged commit 2aeb148 into main May 6, 2026
11 checks passed
@superdav42
Copy link
Copy Markdown
Collaborator Author

Summary

  • Customer reports said exporting a site with Include Plugins / Themes / Uploads ticked produced a ZIP containing only the database dump. Importing the ZIP into a fresh WordPress could not reproduce the original site.
  • Root cause: the WP-CLI gate in inc/site-exporter/mu-migration/includes/helpers.php used PHP_SAPI === 'cli', which is true under PHPUnit (and any other CLI-mode PHP that autoloads wp-cli/wp-cli without bootstrapping WP-CLI). That sent calls into a non-bootstrapped \WP_CLI::runcommand() and raised Undefined constant WP_CLI_ROOT, which the upstream try/catch swallowed — leaving the export with no users.csv/tables.sql/meta.json and no plugin/theme/upload paths, so the zipper produced a DB-only archive. The same trap killed theme enable on import, leaving imported themes on disk but never activated.
  • Fix replaces the gate with the canonical defined('WP_CLI') && WP_CLI check (matching Site_Exporter::setup, trait-wp-cli, and external-cron-manager), adds a theme enable branch to the polyfill, and moves intermediate users/tables/meta files from CWD to sys_get_temp_dir() so locked-down hosts no longer silently drop them.

Bugs fixed

  1. Export produced DB-only ZIPsruncommand() / launch_self() SAPI-only gate engaged \WP_CLI without a live WP-CLI runtime; intermediate files were never written and the zipper saw an empty fileset.
  2. Imported themes never activatedHelpers\runcommand('theme enable', ...) had no polyfill in web/AJAX context.
  3. Intermediate export files left in WordPress root / plugin source tree when the CWD was writable but the export aborted before cleanup; on hosts where CWD was not writable the same files silently failed to write at all.
  4. PHP 8.4 deprecation noise from implicit fputcsv() / fgetcsv() $escape parameter, which would also break round-tripping CSVs in PHP 9.

Files changed

  • inc/site-exporter/mu-migration/includes/helpers.php — fix WP-CLI gate in runcommand() and launch_self(), add theme enable polyfill.
  • inc/site-exporter/mu-migration/includes/commands/class-mu-migration-export.php — write intermediate files to system temp dir; explicit fputcsv() enclosure/escape.
  • inc/site-exporter/mu-migration/includes/commands/class-mu-migration-import.php — explicit fgetcsv() enclosure/escape to match.

Tests added

  • tests/WP_Ultimo/Site_Exporter_Zip_Contents_Test.php — drives ExportCommand::all() with redirected plugin/theme/upload fixtures and asserts the resulting ZIP contains the right entries (wp-content/plugins/<slug>/<file>, wp-content/themes/<slug>/style.css, wp-content/uploads/2024/01/photo.jpg, etc.). Includes a regression test for the existing wu_site_exporter_plugin_exclusion_list filter (ultimate-multisite must be excluded, other plugins preserved).
  • tests/WP_Ultimo/Site_Exporter_Round_Trip_Test.php — exports a fixture site, then extracts the produced ZIP via Helpers\extract() and asserts plugin/upload trees round-trip byte-for-byte.
  • tests/WP_Ultimo/Site_Exporter_Import_Move_Test.php — exercises the private import movers (move_uploads, move_themes) and confirms the new theme enable polyfill calls switch_theme().
    Reverting any single change in helpers.php makes the corresponding test fail with the original Undefined constant WP_CLI_ROOT error or with an unswitched theme.

Verification

$ vendor/bin/phpunit --no-coverage --filter 'Site_Exporter|Exporter_Functions|Round_Trip|Zip_Contents|Import_Move'
............................  64 / 64 (100%)
OK, but incomplete, skipped, or risky tests!
Tests: 64, Assertions: 144, Skipped: 1.

All 64 site-exporter tests pass; pre-existing tests are unchanged.

Test plan for reviewers

  1. Pull this branch into a multisite dev install with WP-CLI not running through cron (i.e. simulate a real admin export by triggering the export from the network admin UI).
  2. From Sites → Export, tick Include Plugins, Include Themes, Include Uploads, submit synchronously.
  3. Download the produced ZIP and run unzip -l export.zip — confirm the listing contains entries under wp-content/plugins/, wp-content/themes/<slug>/, and wp-content/uploads/. Before this PR the listing only contained the .csv, .sql, and .json.
  4. On a fresh WordPress install, import the ZIP via the same UI. Confirm the imported plugins appear under wp-content/plugins, the imported theme is active (not just present), and uploaded media is reachable from the Media library.

aidevops.sh v3.14.78 plugin for OpenCode v1.14.33 with claude-sonnet-4-6 spent 2d 10h on this as a headless worker.


Merged via PR #1140 to main.
Merged by deterministic merge pass (pulse-wrapper.sh).

1 similar comment
@superdav42
Copy link
Copy Markdown
Collaborator Author

Summary

  • Customer reports said exporting a site with Include Plugins / Themes / Uploads ticked produced a ZIP containing only the database dump. Importing the ZIP into a fresh WordPress could not reproduce the original site.
  • Root cause: the WP-CLI gate in inc/site-exporter/mu-migration/includes/helpers.php used PHP_SAPI === 'cli', which is true under PHPUnit (and any other CLI-mode PHP that autoloads wp-cli/wp-cli without bootstrapping WP-CLI). That sent calls into a non-bootstrapped \WP_CLI::runcommand() and raised Undefined constant WP_CLI_ROOT, which the upstream try/catch swallowed — leaving the export with no users.csv/tables.sql/meta.json and no plugin/theme/upload paths, so the zipper produced a DB-only archive. The same trap killed theme enable on import, leaving imported themes on disk but never activated.
  • Fix replaces the gate with the canonical defined('WP_CLI') && WP_CLI check (matching Site_Exporter::setup, trait-wp-cli, and external-cron-manager), adds a theme enable branch to the polyfill, and moves intermediate users/tables/meta files from CWD to sys_get_temp_dir() so locked-down hosts no longer silently drop them.

Bugs fixed

  1. Export produced DB-only ZIPsruncommand() / launch_self() SAPI-only gate engaged \WP_CLI without a live WP-CLI runtime; intermediate files were never written and the zipper saw an empty fileset.
  2. Imported themes never activatedHelpers\runcommand('theme enable', ...) had no polyfill in web/AJAX context.
  3. Intermediate export files left in WordPress root / plugin source tree when the CWD was writable but the export aborted before cleanup; on hosts where CWD was not writable the same files silently failed to write at all.
  4. PHP 8.4 deprecation noise from implicit fputcsv() / fgetcsv() $escape parameter, which would also break round-tripping CSVs in PHP 9.

Files changed

  • inc/site-exporter/mu-migration/includes/helpers.php — fix WP-CLI gate in runcommand() and launch_self(), add theme enable polyfill.
  • inc/site-exporter/mu-migration/includes/commands/class-mu-migration-export.php — write intermediate files to system temp dir; explicit fputcsv() enclosure/escape.
  • inc/site-exporter/mu-migration/includes/commands/class-mu-migration-import.php — explicit fgetcsv() enclosure/escape to match.

Tests added

  • tests/WP_Ultimo/Site_Exporter_Zip_Contents_Test.php — drives ExportCommand::all() with redirected plugin/theme/upload fixtures and asserts the resulting ZIP contains the right entries (wp-content/plugins/<slug>/<file>, wp-content/themes/<slug>/style.css, wp-content/uploads/2024/01/photo.jpg, etc.). Includes a regression test for the existing wu_site_exporter_plugin_exclusion_list filter (ultimate-multisite must be excluded, other plugins preserved).
  • tests/WP_Ultimo/Site_Exporter_Round_Trip_Test.php — exports a fixture site, then extracts the produced ZIP via Helpers\extract() and asserts plugin/upload trees round-trip byte-for-byte.
  • tests/WP_Ultimo/Site_Exporter_Import_Move_Test.php — exercises the private import movers (move_uploads, move_themes) and confirms the new theme enable polyfill calls switch_theme().
    Reverting any single change in helpers.php makes the corresponding test fail with the original Undefined constant WP_CLI_ROOT error or with an unswitched theme.

Verification

$ vendor/bin/phpunit --no-coverage --filter 'Site_Exporter|Exporter_Functions|Round_Trip|Zip_Contents|Import_Move'
............................  64 / 64 (100%)
OK, but incomplete, skipped, or risky tests!
Tests: 64, Assertions: 144, Skipped: 1.

All 64 site-exporter tests pass; pre-existing tests are unchanged.

Test plan for reviewers

  1. Pull this branch into a multisite dev install with WP-CLI not running through cron (i.e. simulate a real admin export by triggering the export from the network admin UI).
  2. From Sites → Export, tick Include Plugins, Include Themes, Include Uploads, submit synchronously.
  3. Download the produced ZIP and run unzip -l export.zip — confirm the listing contains entries under wp-content/plugins/, wp-content/themes/<slug>/, and wp-content/uploads/. Before this PR the listing only contained the .csv, .sql, and .json.
  4. On a fresh WordPress install, import the ZIP via the same UI. Confirm the imported plugins appear under wp-content/plugins, the imported theme is active (not just present), and uploaded media is reachable from the Media library.

aidevops.sh v3.14.78 plugin for OpenCode v1.14.33 with claude-sonnet-4-6 spent 2d 10h on this as a headless worker.


Merged via PR #1140 to main.
Merged by deterministic merge pass (pulse-wrapper.sh).

@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 6, 2026

Performance Test Results

Performance test results for a290e4c are in 🛎️!

URL: /

Run DB Queries Memory Before Template Template WP Total LCP TTFB LCP - TTFB
0 37 37.71 MB 823.50 ms 170.50 ms 1017.50 ms 1954.00 ms 1862.80 ms 88.85 ms
1 56 49.11 MB 928.00 ms 144.00 ms 1075.50 ms 2028.00 ms 1946.40 ms 82.15 ms

@superdav42 superdav42 deleted the opencode/witty-circuit branch May 6, 2026 15:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant