Skip to content

fix: purge Divi clone CSS cache#1352

Merged
superdav42 merged 2 commits into
mainfrom
fix/divi-clone-cache-purge
Jun 6, 2026
Merged

fix: purge Divi clone CSS cache#1352
superdav42 merged 2 commits into
mainfrom
fix/divi-clone-cache-purge

Conversation

@superdav42

@superdav42 superdav42 commented Jun 6, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Purge the cloned site's Divi static CSS cache on wu_duplicate_site.
  • Delete only the cloned blog's wp-content/et-cache/{network_id}/{blog_id}/ directory, with a legacy et-cache/{blog_id} fallback.
  • Add focused coverage that confirms the hook is registered and only the cloned site's cache directory is removed.

Verification

  • php -l inc/compat/class-general-compat.php
  • php -l tests/WP_Ultimo/General_Compat_Test.php
  • vendor/bin/phpcs inc/compat/class-general-compat.php tests/WP_Ultimo/General_Compat_Test.php
  • vendor/bin/phpstan analyse inc/compat/class-general-compat.php tests/WP_Ultimo/General_Compat_Test.php
  • vendor/bin/phpunit --filter General_Compat_Test

Notes

The customer report showed a cloned Divi page loading all assets successfully but using incomplete generated Divi layout CSS. Purging the cloned site's et-cache directory forces Divi to regenerate the CSS with the copied builder data on the next frontend request.


Summary by CodeRabbit

  • Bug Fixes

    • Ensure Divi static CSS cache is cleared when a site is duplicated so cloned sites load correct styles without impacting the original.
  • Tests

    • Added tests to verify the Divi cache cleanup runs on duplication and only removes cache for the cloned site.
  • Chores

    • CI: Updated performance workflow to gracefully skip performance metrics when browser installation fails to avoid spurious failures.

@superdav42 superdav42 added status:in-review PR open, awaiting review/merge origin:interactive Created by interactive user session labels Jun 6, 2026
@coderabbitai

coderabbitai Bot commented Jun 6, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 11d3794f-71f3-4e45-9570-84bd8c960b54

📥 Commits

Reviewing files that changed from the base of the PR and between 70db972 and 4b1ef20.

📒 Files selected for processing (1)
  • .github/workflows/tests.yml

📝 Walkthrough

Walkthrough

Adds Divi compatibility to site duplication by clearing the duplicated site's Divi et-cache directories (with path validation and recursive deletion) and adds tests. Separately, updates the CI wp-performance workflow to skip Playwright-based performance steps when the Playwright install times out or fails.

Changes

Divi Cache Clearing on Site Duplication

Layer / File(s) Summary
Divi cache-clearing hook and implementation
inc/compat/class-general-compat.php
General_Compat registers a wu_duplicate_site hook callback that switches to the duplicated blog and safely removes Divi's et-cache directories using realpath-based containment validation and recursive cleanup, with fallback support for legacy cache layouts.
Test suite for cache-clearing behavior
tests/WP_Ultimo/General_Compat_Test.php
A new test class validates hook registration and confirms cache directories are deleted only for the cloned site in multisite environments, using a recursive directory helper and fixture setup/teardown to ensure test isolation.

Conditional Performance Steps in CI

Layer / File(s) Summary
Playwright install timeout and PERF_SKIP flag
.github/workflows/tests.yml
Adds a timed Playwright install step that sets PERF_SKIP=true on failure to signal skipping performance metric execution.
Gate performance steps with PERF_SKIP
.github/workflows/tests.yml
Gates merged blueprint preparation, baseline/PR performance runs, server lifecycle steps, and report generation with if: env.PERF_SKIP != 'true', preserving always() where cleanup/reporting must still run conditionally.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Poem

🐰 I hopped through code with a twitchy nose,
Cleared Divi cache where the cloned site grows,
CI skips the tests when Playwright stalls,
Fresh builds, fresh styles, no stale cache calls!

🚥 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 'fix: purge Divi clone CSS cache' directly aligns with the main change: implementing a cache purge mechanism for Divi's static CSS when sites are duplicated.
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 unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/divi-clone-cache-purge

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

github-actions Bot commented Jun 6, 2026

Copy link
Copy Markdown

🔨 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

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 2

🧹 Nitpick comments (1)
tests/WP_Ultimo/General_Compat_Test.php (1)

52-79: ⚡ Quick win

Cover the legacy Divi cache layout too.

The production code also removes wp-content/et-cache/{blog_id} as a fallback, but this test only exercises et-cache/{network_id}/{blog_id}. Add a legacy fixture/assertion here so the fallback path cannot regress silently.

Suggested coverage expansion
 		$network_id    = (int) get_current_network_id();
 		$cache_root    = trailingslashit(WP_CONTENT_DIR) . 'et-cache';
 		$cache_dir     = trailingslashit($cache_root) . $network_id . '/' . $blog_id;
+		$legacy_dir    = trailingslashit($cache_root) . $blog_id;
 		$other_dir     = trailingslashit($cache_root) . $network_id . '/' . $other_blog_id;
 
-		$this->cache_dirs = [$cache_dir, $other_dir];
+		$this->cache_dirs = [$cache_dir, $legacy_dir, $other_dir];
 
 		wp_mkdir_p($cache_dir . '/9');
+		wp_mkdir_p($legacy_dir . '/9');
 		wp_mkdir_p($other_dir . '/9');
 
 		// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents -- Test fixture setup.
 		file_put_contents($cache_dir . '/9/et-core-unified-deferred-9.min.css', 'stale divi css');
 		// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents -- Test fixture setup.
+		file_put_contents($legacy_dir . '/9/et-core-unified-deferred-9.min.css', 'legacy divi css');
+		// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents -- Test fixture setup.
 		file_put_contents($other_dir . '/9/et-core-unified-deferred-9.min.css', 'other divi css');
 
 		General_Compat::get_instance()->clear_divi_static_css_cache(['site_id' => $blog_id]);
 
 		$this->assertDirectoryDoesNotExist($cache_dir);
+		$this->assertDirectoryDoesNotExist($legacy_dir);
 		$this->assertDirectoryExists($other_dir);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/WP_Ultimo/General_Compat_Test.php` around lines 52 - 79, Extend the
test_clear_divi_static_css_cache_deletes_cloned_site_cache_only test to also
create the legacy cache layout (wp-content/et-cache/{blog_id}) alongside the
networked layout, write a fixture file (e.g. et-core-unified-deferred-9.min.css)
into that legacy dir, then call
General_Compat::get_instance()->clear_divi_static_css_cache(['site_id' =>
$blog_id]) and assert the legacy cache directory is removed while the other
blog's legacy/networked dirs still exist; update the test's $this->cache_dirs
setup and add corresponding file creation and assertions so the fallback removal
path is covered.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@inc/compat/class-general-compat.php`:
- Around line 448-479: The purge currently uses $file->getRealPath() which
resolves symlinks and can cause deletions outside $real_cache_dir; change the
loop to use $file->getPathname(), skip any symlink entries (use
SplFileInfo::isLink()/isLink), normalize the pathname (wp_normalize_path +
untrailingslashit/trailingslashit) and then verify the normalized path has the
$real_cache_dir prefix before deleting; keep the existing behavior for
directories (rmdir) and files (wp_delete_file()) but only after the symlink
check and prefix re-check to prevent path escape.

In `@tests/WP_Ultimo/General_Compat_Test.php`:
- Around line 1-17: Add the missing file guard and update the test namespace to
mirror the source: insert defined('ABSPATH') || exit; at the top of the file
immediately after <?php, and change the namespace from WP_Ultimo\Tests to
WP_Ultimo\Compat\Tests so the test namespace mirrors the source class
WP_Ultimo\Compat\General_Compat; keep the existing use/imports and class name
General_Compat_Test unchanged.

---

Nitpick comments:
In `@tests/WP_Ultimo/General_Compat_Test.php`:
- Around line 52-79: Extend the
test_clear_divi_static_css_cache_deletes_cloned_site_cache_only test to also
create the legacy cache layout (wp-content/et-cache/{blog_id}) alongside the
networked layout, write a fixture file (e.g. et-core-unified-deferred-9.min.css)
into that legacy dir, then call
General_Compat::get_instance()->clear_divi_static_css_cache(['site_id' =>
$blog_id]) and assert the legacy cache directory is removed while the other
blog's legacy/networked dirs still exist; update the test's $this->cache_dirs
setup and add corresponding file creation and assertions so the fallback removal
path is covered.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 4ce5ca9d-4d26-43fb-b539-3f709fcb4a5a

📥 Commits

Reviewing files that changed from the base of the PR and between 63dc3b9 and 70db972.

📒 Files selected for processing (2)
  • inc/compat/class-general-compat.php
  • tests/WP_Ultimo/General_Compat_Test.php

Comment on lines +448 to +479
$real_cache_dir = realpath($cache_dir);
$real_cache_root = realpath($cache_root);

if (false === $real_cache_dir || false === $real_cache_root) {
return;
}

$real_cache_dir = untrailingslashit(wp_normalize_path($real_cache_dir));
$real_cache_root = untrailingslashit(wp_normalize_path($real_cache_root));

if ($real_cache_root === $real_cache_dir || 0 !== strpos(trailingslashit($real_cache_dir), trailingslashit($real_cache_root))) {
return;
}

$iterator = new \RecursiveIteratorIterator(
new \RecursiveDirectoryIterator($real_cache_dir, \FilesystemIterator::SKIP_DOTS),
\RecursiveIteratorIterator::CHILD_FIRST
);

foreach ($iterator as $file) {
$path = $file->getRealPath();

if (false === $path) {
continue;
}

if ($file->isDir()) {
// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_rmdir, WordPress.PHP.NoSilencedErrors.Discouraged -- Removes an empty generated cache directory after deleting its contents.
@rmdir($path);
} else {
wp_delete_file($path);
}

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.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

Does PHP SplFileInfo::getRealPath()resolve symlink targets, and doesRecursiveDirectoryIteratorstill yield symlink entries whenFilesystemIterator::FOLLOW_SYMLINKS is not set?

💡 Result:

Yes, PHP's SplFileInfo::getRealPath resolves all symbolic links [1][2]. It expands symlinks, resolves relative references (like /./ or /../), and returns the canonicalized absolute path to the file [3][2]. If the file does not exist, the method returns false [1][2]. Regarding RecursiveDirectoryIterator, it does still yield symlink entries even when the FilesystemIterator::FOLLOW_SYMLINKS flag is not set [4][5]. The iterator will return the symlink itself as an entry in the iteration [6]. The FilesystemIterator::FOLLOW_SYMLINKS flag controls whether the iterator should follow these symlinks to traverse into the target directories, but it does not prevent the iterator from encountering and yielding the symlink files themselves [7][4].

Citations:


Skip symlinks when purging cache contents to prevent path escape

SplFileInfo::getRealPath() resolves symlink targets, and RecursiveDirectoryIterator still yields symlink entries even without FOLLOW_SYMLINKS. A symlink inside $real_cache_dir can therefore make wp_delete_file() act on a path outside the cache tree. Use getPathname(), skip links, and re-check each entry against a $real_cache_dir prefix before deleting.

Suggested hardening
 		$real_cache_dir  = untrailingslashit(wp_normalize_path($real_cache_dir));
 		$real_cache_root = untrailingslashit(wp_normalize_path($real_cache_root));
 
 		if ($real_cache_root === $real_cache_dir || 0 !== strpos(trailingslashit($real_cache_dir), trailingslashit($real_cache_root))) {
 			return;
 		}
+
+		$real_cache_dir_prefix = trailingslashit($real_cache_dir);
 
 		$iterator = new \RecursiveIteratorIterator(
 			new \RecursiveDirectoryIterator($real_cache_dir, \FilesystemIterator::SKIP_DOTS),
 			\RecursiveIteratorIterator::CHILD_FIRST
 		);
 
 		foreach ($iterator as $file) {
-			$path = $file->getRealPath();
+			if ($file->isLink()) {
+				continue;
+			}
 
-			if (false === $path) {
+			$path = untrailingslashit(wp_normalize_path($file->getPathname()));
+
+			if ('' === $path || 0 !== strpos(trailingslashit($path), $real_cache_dir_prefix)) {
 				continue;
 			}
 
 			if ($file->isDir()) {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@inc/compat/class-general-compat.php` around lines 448 - 479, The purge
currently uses $file->getRealPath() which resolves symlinks and can cause
deletions outside $real_cache_dir; change the loop to use $file->getPathname(),
skip any symlink entries (use SplFileInfo::isLink()/isLink), normalize the
pathname (wp_normalize_path + untrailingslashit/trailingslashit) and then verify
the normalized path has the $real_cache_dir prefix before deleting; keep the
existing behavior for directories (rmdir) and files (wp_delete_file()) but only
after the symlink check and prefix re-check to prevent path escape.

Comment on lines +1 to +17
<?php
/**
* Tests for the General_Compat class.
*
* @package WP_Ultimo
* @subpackage Tests
*/

namespace WP_Ultimo\Tests;

use WP_Ultimo\Compat\General_Compat;
use WP_UnitTestCase;

/**
* Test general compatibility fixes.
*/
class General_Compat_Test extends WP_UnitTestCase {

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.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Match the repo's PHP test-file conventions.

This file is missing the standard defined('ABSPATH') || exit; guard, and namespace WP_Ultimo\Tests; does not mirror the source namespace for WP_Ultimo\Compat\General_Compat.

Suggested cleanup
 <?php
+defined('ABSPATH') || exit;
+
 /**
  * Tests for the General_Compat class.
  *
  * `@package` WP_Ultimo
  * `@subpackage` Tests
  */
 
-namespace WP_Ultimo\Tests;
+namespace WP_Ultimo\Compat;
 
-use WP_Ultimo\Compat\General_Compat;
 use WP_UnitTestCase;

As per coding guidelines, Every PHP file must start with defined('ABSPATH') || exit; and tests/WP_Ultimo/**/*_Test.php: Test namespace must mirror source structure.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<?php
/**
* Tests for the General_Compat class.
*
* @package WP_Ultimo
* @subpackage Tests
*/
namespace WP_Ultimo\Tests;
use WP_Ultimo\Compat\General_Compat;
use WP_UnitTestCase;
/**
* Test general compatibility fixes.
*/
class General_Compat_Test extends WP_UnitTestCase {
<?php
defined('ABSPATH') || exit;
/**
* Tests for the General_Compat class.
*
* `@package` WP_Ultimo
* `@subpackage` Tests
*/
namespace WP_Ultimo\Compat;
use WP_UnitTestCase;
/**
* Test general compatibility fixes.
*/
class General_Compat_Test extends WP_UnitTestCase {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/WP_Ultimo/General_Compat_Test.php` around lines 1 - 17, Add the missing
file guard and update the test namespace to mirror the source: insert
defined('ABSPATH') || exit; at the top of the file immediately after <?php, and
change the namespace from WP_Ultimo\Tests to WP_Ultimo\Compat\Tests so the test
namespace mirrors the source class WP_Ultimo\Compat\General_Compat; keep the
existing use/imports and class name General_Compat_Test unchanged.

Source: Coding guidelines

@github-actions

github-actions Bot commented Jun 6, 2026

Copy link
Copy Markdown

🔨 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 70e3c57 into main Jun 6, 2026
11 checks passed
@superdav42 superdav42 deleted the fix/divi-clone-cache-purge branch June 6, 2026 18:15
@superdav42 superdav42 added the review-feedback-scanned Merged PR already scanned for quality feedback label Jun 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

origin:interactive Created by interactive user session review-feedback-scanned Merged PR already scanned for quality feedback status:in-review PR open, awaiting review/merge

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant