Skip to content
Open
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 44 additions & 6 deletions inc/common/purge.php
Original file line number Diff line number Diff line change
Expand Up @@ -131,11 +131,15 @@
$purge_urls[] = $author_url;
}

// Add all parents.
$parents = get_post_ancestors( $post_id );
if ( (bool) $parents ) {
foreach ( $parents as $parent_id ) {
$purge_urls[] = get_permalink( $parent_id );
$post_name = get_post_field( 'post_name', $post_id );
// If the slug has changed, add all children pages with the new slug.
if ( $post_name != $post_data['post_name'] ) {

Check warning on line 136 in inc/common/purge.php

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

inc/common/purge.php#L136

Avoid unused local variables such as '$post_data'.

Check warning on line 136 in inc/common/purge.php

View workflow job for this annotation

GitHub Actions / lint / PHP CodeSniffer

Loose comparisons are not allowed. Expected: "!=="; Found: "!="

Check warning on line 136 in inc/common/purge.php

View workflow job for this annotation

GitHub Actions / WP latest with PHP 8.4 on ubuntu-latest.

Trying to access array offset on null

Check warning on line 136 in inc/common/purge.php

View workflow job for this annotation

GitHub Actions / WP latest with PHP 8.4 on ubuntu-latest.

Undefined variable $post_data

Check warning on line 136 in inc/common/purge.php

View workflow job for this annotation

GitHub Actions / WP latest with PHP 8.4 on ubuntu-latest.

Trying to access array offset on null

Check warning on line 136 in inc/common/purge.php

View workflow job for this annotation

GitHub Actions / WP latest with PHP 8.4 on ubuntu-latest.

Undefined variable $post_data
// Add all children.
$children = rocket_get_all_descendant( $post_id );
if ( (bool) $children ) {
foreach ( $children as $child_id ) {
$purge_urls[] = get_permalink( $child_id );
}
}
}

Expand All @@ -146,6 +150,27 @@
}
}

/**
* Recursively retrieves all descendant page IDs for a given parent page ID.
*
* @param int $parent_id The ID of the parent page.
* @return int[] A flat array with all descendant page IDs (children, grandchildren, etc.).
*/
function rocket_get_all_descendant( $parent_id ) {
// 'child_of' retrieves all descendants, not just direct children.
$all_descendants = get_pages(
[ 'child_of' => $parent_id ]

Check notice on line 162 in inc/common/purge.php

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

inc/common/purge.php#L162

Whitespace found at end of line

Check failure on line 162 in inc/common/purge.php

View workflow job for this annotation

GitHub Actions / lint / PHP CodeSniffer

Whitespace found at end of line
);

if ( empty( $all_descendants ) ) {
return [];
}

// Convert the array of page objects to an array of IDs only.
return wp_list_pluck( $all_descendants, 'ID' );
}


/**
* Update cache when a post is updated or commented
*
Expand Down Expand Up @@ -636,6 +661,19 @@
if ( empty( $post_name ) ) {
return;
}
rocket_clean_files( get_the_permalink( $post_id ) );

$purge_urls = [];
$purge_urls[] = get_the_permalink( $post_id );

// Clear cache for all child pages.
$children = rocket_get_all_descendant( $post_id );
if ( (bool) $children ) {
foreach ( $children as $child_id ) {
$purge_urls[] = get_the_permalink( $child_id );
}
}

rocket_clean_files( $purge_urls );
Comment on lines +665 to +676
Copy link

Copilot AI Jan 26, 2026

Choose a reason for hiding this comment

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

rocket_clean_post_cache_on_slug_change() now purges an array of URLs (parent + all descendants) instead of a single permalink, and it introduces new behavior (child pages being cleared when a parent slug changes), but existing unit/integration tests for this function currently only assert a single call to rocket_clean_files() with one URL and don’t verify that child URLs are included. Please update those tests to expect the new array-based call signature and add assertions/fixtures that cover the descendant purge behavior so this logic is protected against regressions.

Copilot generated this review using guidance from repository custom instructions.

}

Check failure on line 678 in inc/common/purge.php

View workflow job for this annotation

GitHub Actions / lint / PHP CodeSniffer

Function closing brace must go on the next line following the body; found 1 blank lines before brace
add_action( 'pre_post_update', 'rocket_clean_post_cache_on_slug_change', PHP_INT_MAX, 2 );
145 changes: 108 additions & 37 deletions inc/functions/files.php
Original file line number Diff line number Diff line change
Expand Up @@ -530,6 +530,28 @@
return get_rocket_parse_url( $new_permalink );
}


/**
* Count the number of path segments in a URL.
*
* Parses the given URL, extracts its path component, trims leading/trailing
* slashes, and counts the remaining segments separated by '/'.
*
* Examples:
* - "https://example.com/" => 0
* - "https://example.com/a/b/c" => 3
* - "/a/b/" => 2
*
* @param string $url The URL (absolute or relative) to analyze.
* @return int Number of segments in the URL path (0 if empty or no path).
*/
function rocket_count_path_segments(string $url): int {

Check notice on line 548 in inc/functions/files.php

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

inc/functions/files.php#L548

Expected 1 spaces after opening parenthesis; 0 found

Check failure on line 548 in inc/functions/files.php

View workflow job for this annotation

GitHub Actions / lint / PHP CodeSniffer

Expected 1 spaces before closing parenthesis; 0 found

Check failure on line 548 in inc/functions/files.php

View workflow job for this annotation

GitHub Actions / lint / PHP CodeSniffer

Expected 1 spaces after opening parenthesis; 0 found
$path = parse_url($url, PHP_URL_PATH) ?? '';

Check notice on line 549 in inc/functions/files.php

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

inc/functions/files.php#L549

Expected 1 spaces after opening parenthesis; 0 found

Check notice on line 549 in inc/functions/files.php

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

inc/functions/files.php#L549

Tabs must be used to indent lines; spaces are not allowed

Check warning on line 549 in inc/functions/files.php

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

inc/functions/files.php#L549

parse_url() is discouraged because of inconsistency in the output across PHP versions; use wp_parse_url() instead.

Check failure on line 549 in inc/functions/files.php

View workflow job for this annotation

GitHub Actions / lint / PHP CodeSniffer

Expected 1 spaces before closing parenthesis; 0 found

Check failure on line 549 in inc/functions/files.php

View workflow job for this annotation

GitHub Actions / lint / PHP CodeSniffer

Expected 1 spaces after opening parenthesis; 0 found

Check warning on line 549 in inc/functions/files.php

View workflow job for this annotation

GitHub Actions / lint / PHP CodeSniffer

parse_url() is discouraged because of inconsistency in the output across PHP versions; use wp_parse_url() instead.

Check failure on line 549 in inc/functions/files.php

View workflow job for this annotation

GitHub Actions / lint / PHP CodeSniffer

Tabs must be used to indent lines; spaces are not allowed
$path = trim($path, '/'); // "/" -> ""

Check notice on line 550 in inc/functions/files.php

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

inc/functions/files.php#L550

Expected 1 spaces after opening parenthesis; 0 found

Check notice on line 550 in inc/functions/files.php

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

inc/functions/files.php#L550

Tabs must be used to indent lines; spaces are not allowed

Check failure on line 550 in inc/functions/files.php

View workflow job for this annotation

GitHub Actions / lint / PHP CodeSniffer

Expected 1 spaces before closing parenthesis; 0 found

Check failure on line 550 in inc/functions/files.php

View workflow job for this annotation

GitHub Actions / lint / PHP CodeSniffer

Expected 1 spaces after opening parenthesis; 0 found

Check failure on line 550 in inc/functions/files.php

View workflow job for this annotation

GitHub Actions / lint / PHP CodeSniffer

Tabs must be used to indent lines; spaces are not allowed
if ($path === '') return 0;

Check notice on line 551 in inc/functions/files.php

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

inc/functions/files.php#L551

Inline control structures are not allowed

Check notice on line 551 in inc/functions/files.php

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

inc/functions/files.php#L551

Tabs must be used to indent lines; spaces are not allowed

Check notice on line 551 in inc/functions/files.php

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

inc/functions/files.php#L551

Use Yoda Condition checks, you must.
return count(explode('/', $path)); // "a/b/c" -> 3

Check notice on line 552 in inc/functions/files.php

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

inc/functions/files.php#L552

Expected 1 spaces after opening parenthesis; 0 found

Check notice on line 552 in inc/functions/files.php

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

inc/functions/files.php#L552

Tabs must be used to indent lines; spaces are not allowed
}

/**
* Delete one or several cache files.
*
Expand Down Expand Up @@ -563,6 +585,24 @@
$filesystem = rocket_direct_filesystem();
}

// Sort: most segments first (deepest URLs first).
usort(
$urls,
static function ( $a, $b ) {
$da = rocket_count_path_segments( (string) $a );
$db = rocket_count_path_segments( (string) $b );

// First by depth (descending)

Check notice on line 595 in inc/functions/files.php

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

inc/functions/files.php#L595

Inline comments must end in full-stops, exclamation marks, or question marks
if ( $da !== $db ) {
return $db <=> $da;
}

// If depth is equal, alphabetically

Check notice on line 600 in inc/functions/files.php

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

inc/functions/files.php#L600

Inline comments must end in full-stops, exclamation marks, or question marks
return strcmp( (string) $a, (string) $b );
}
);
Comment on lines +592 to +607
Copy link

Copilot AI Jan 26, 2026

Choose a reason for hiding this comment

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

Introducing the usort() here changes the order of $urls before firing the before_rocket_clean_files and before_rocket_clean_file actions, and it also means deeper URLs are processed first. Existing tests and 3rd-party code may implicitly rely on the previous iteration order, so it would be good to (1) update the unit tests for rocket_clean_files() to assert the new ordering semantics where relevant and (2) document in tests/fixtures that purge order is depth-first (deepest paths first) to guard against accidental reordering in future refactors.

Copilot generated this review using guidance from repository custom instructions.

Check notice on line 604 in inc/functions/files.php

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

inc/functions/files.php#L604

Functions must not contain multiple empty lines in a row; found 2 empty lines

if ( $run_actions ) {
/**
* Fires before all cache files are deleted.
Expand All @@ -575,6 +615,7 @@
}

foreach ( $urls as $url_key => $url ) {

if ( $run_actions ) {
/**
* Fires before the cache file is deleted.
Expand All @@ -591,55 +632,85 @@

$parsed_url = get_rocket_parse_url( $url );

if ( ! empty( $parsed_url['host'] ) ) {
foreach ( _rocket_get_cache_dirs( $parsed_url['host'], $cache_path ) as $dir ) {
// Decode url path.
$url_chunks = explode( '/', $parsed_url['path'] );
$matches = preg_grep( '/%/', $url_chunks );
if ( empty( $parsed_url['host'] ) ) {
continue;
}

if ( ! empty( $matches ) ) {
$parsed_url['path'] = rawurldecode( $parsed_url['path'] );
}
foreach ( _rocket_get_cache_dirs( $parsed_url['host'], $cache_path ) as $dir ) {
// Decode url path.
$url_chunks = explode( '/', $parsed_url['path'] );
$matches = preg_grep( '/%/', $url_chunks );

// Encode Non-latin characters if found in url path.
if ( false !== preg_match_all( '/(?<non_latin>[^\x00-\x7F]+)/', $parsed_url['path'], $matches ) ) {
$cb_encode_non_latin = function ( $non_latin ) {
return strtolower( rawurlencode( $non_latin ) );
};
if ( ! empty( $matches ) ) {
$parsed_url['path'] = rawurldecode( $parsed_url['path'] );
}

$parsed_url['path'] = str_replace( $matches['non_latin'], array_map( $cb_encode_non_latin, $matches['non_latin'] ), $parsed_url['path'] );
}
// Encode Non-latin characters if found in url path.
if ( false !== preg_match_all( '/(?<non_latin>[^\x00-\x7F]+)/', $parsed_url['path'], $matches ) ) {
$cb_encode_non_latin = function ( $non_latin ) {
return strtolower( rawurlencode( $non_latin ) );
};

$entry = $dir . $parsed_url['path'];

// For regex we use it for file names only, and it should include the * character.
if ( str_contains( $entry, '*' ) ) {
$regex_part = basename( $entry );
$search_dir = str_replace( $regex_part, '', $entry );
$matched_files = _rocket_get_dir_files_by_regex( $search_dir, '#' . $regex_part . '#i' );
foreach ( $matched_files as $item ) {
$current_file = $item->getPath() . DIRECTORY_SEPARATOR . $item->getFilename();
if ( $filesystem->exists( $current_file ) ) {
$filesystem->delete( $current_file );
}
$parsed_url['path'] = str_replace( $matches['non_latin'], array_map( $cb_encode_non_latin, $matches['non_latin'] ), $parsed_url['path'] );
}

$entry = $dir . $parsed_url['path'];

// For regex we use it for file names only, and it should include the * character.
if ( str_contains( $entry, '*' ) ) {
$regex_part = basename( $entry );
$search_dir = str_replace( $regex_part, '', $entry );
$matched_files = _rocket_get_dir_files_by_regex( $search_dir, '#' . $regex_part . '#i' );
foreach ( $matched_files as $item ) {
$current_file = $item->getPath() . DIRECTORY_SEPARATOR . $item->getFilename();
if ( $filesystem->exists( $current_file ) ) {
$filesystem->delete( $current_file );
}
// Remove the regex part from the url.
$url = str_replace( $regex_part, '', $url );
$urls[ $url_key ] = $url;
}
// Remove the regex part from the url.
$url = str_replace( $regex_part, '', $url );
$urls[ $url_key ] = $url;
}

// Skip if the dir/file does not exist.
if ( ! $filesystem->exists( $entry ) ) {
continue;
// Skip if the dir/file does not exist.
if ( ! $filesystem->exists( $entry ) ) {
continue;
}

if ( ! $filesystem->is_dir( $entry ) ) {
$filesystem->delete( $entry );
continue;
}

// Check whether the directory contains subfolders (vs only files).
$has_subdirs = false;
try {
foreach ( new FilesystemIterator( $entry, FilesystemIterator::SKIP_DOTS ) as $child ) {
if ( $child->isDir() ) {
$has_subdirs = true;
break;
}
}
} catch ( Exception $e ) {
// If we can't inspect the directory, be conservative and only delete files.
$has_subdirs = true;
}

if ( $filesystem->is_dir( $entry ) ) {
rocket_rrmdir( $entry, [], $filesystem );
} else {
$filesystem->delete( $entry );
if ( ! $has_subdirs ) {
// Directory contains only files: remove it entirely.
rocket_rrmdir( $entry, [], $filesystem );
continue;
}

// Directory contains subfolders: delete only the files in the top-level.
foreach ( _rocket_get_dir_files_by_regex( $entry, '#.+#' ) as $child ) {
if ( $child->isFile() ) {
Comment on lines +688 to +710
Copy link

Copilot AI Jan 26, 2026

Choose a reason for hiding this comment

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

This block changes rocket_clean_files() semantics in a non-trivial way: instead of always calling rocket_rrmdir() on matching directories, it now conditionally deletes entire directories only when they have no subdirectories and otherwise removes only top-level files. Given how widely rocket_clean_files() is used (including deprecated helpers like rocket_clean_directory_for_default_language_on_wpml()), please extend the existing unit/integration tests for rocket_clean_files() to cover these new branches, including cases where a cache directory contains nested subfolders that must be preserved and verifying that only the intended files are removed.

Copilot generated this review using guidance from repository custom instructions.
$filesystem->delete( $child->getPathname() );
}
}

}

if ( $run_actions ) {
/**
* Fires after the cache file is deleted.
Expand Down
Loading