diff --git a/inc/compat/class-general-compat.php b/inc/compat/class-general-compat.php index 4584784e9..9f96334b4 100644 --- a/inc/compat/class-general-compat.php +++ b/inc/compat/class-general-compat.php @@ -465,17 +465,35 @@ private function delete_divi_static_css_cache_directory(string $cache_dir): void ); foreach ($iterator as $file) { - $path = $file->getRealPath(); + $path = $file->getPathname(); + $normalized_path = untrailingslashit(wp_normalize_path($path)); - if (false === $path) { + if ($real_cache_dir !== $normalized_path && 0 !== strpos(trailingslashit($normalized_path), trailingslashit($real_cache_dir))) { + continue; + } + + if ($file->isLink()) { + wp_delete_file($path); + continue; + } + + $real_path = $file->getRealPath(); + + if (false === $real_path) { + continue; + } + + $real_path = untrailingslashit(wp_normalize_path($real_path)); + + if ($real_cache_dir !== $real_path && 0 !== strpos(trailingslashit($real_path), trailingslashit($real_cache_dir))) { 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); + @rmdir($real_path); } else { - wp_delete_file($path); + wp_delete_file($real_path); } } diff --git a/tests/WP_Ultimo/General_Compat_Test.php b/tests/WP_Ultimo/General_Compat_Test.php index 8af594914..3f2fc989d 100644 --- a/tests/WP_Ultimo/General_Compat_Test.php +++ b/tests/WP_Ultimo/General_Compat_Test.php @@ -6,9 +6,10 @@ * @subpackage Tests */ -namespace WP_Ultimo\Tests; +namespace WP_Ultimo\Compat; + +defined('ABSPATH') || exit; -use WP_Ultimo\Compat\General_Compat; use WP_UnitTestCase; /** @@ -78,6 +79,47 @@ public function test_clear_divi_static_css_cache_deletes_cloned_site_cache_only( $this->assertDirectoryExists($other_dir); } + /** + * Test Divi et-cache symlinks do not delete files outside the cache tree. + */ + public function test_clear_divi_static_css_cache_does_not_follow_symlinks(): void { + + if ( ! is_multisite()) { + $this->markTestSkipped('Divi cache purge tests require multisite'); + } + + if ( ! function_exists('symlink')) { + $this->markTestSkipped('Symlink support is not available'); + } + + $blog_id = self::factory()->blog->create(); + $other_blog_id = self::factory()->blog->create(); + $network_id = (int) get_current_network_id(); + $cache_root = trailingslashit(WP_CONTENT_DIR) . 'et-cache'; + $cache_dir = trailingslashit($cache_root) . $network_id . '/' . $blog_id; + $other_dir = trailingslashit($cache_root) . $network_id . '/' . $other_blog_id; + $outside_file = $other_dir . '/external-target.css'; + $symlink = $cache_dir . '/external-target.css'; + + $this->cache_dirs = [$cache_dir, $other_dir]; + + wp_mkdir_p($cache_dir); + wp_mkdir_p($other_dir); + + // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents -- Test fixture setup. + file_put_contents($outside_file, 'external divi css'); + + // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged, WordPress.WP.AlternativeFunctions.symlink_symlink -- Test fixture setup requires a symlink. + if ( ! @symlink($outside_file, $symlink)) { + $this->markTestSkipped('Symlink fixture could not be created'); + } + + General_Compat::get_instance()->clear_divi_static_css_cache(['site_id' => $blog_id]); + + $this->assertFileExists($outside_file); + $this->assertDirectoryDoesNotExist($cache_dir); + } + /** * Recursively remove a test directory. * @@ -96,7 +138,7 @@ private function remove_directory(string $dir): void { ); foreach ($iterator as $file) { - $path = $file->getRealPath(); + $path = $file->isLink() ? $file->getPathname() : $file->getRealPath(); if (false === $path) { continue;