Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
27 changes: 27 additions & 0 deletions .phpcs.xml.dist
Original file line number Diff line number Diff line change
Expand Up @@ -56,4 +56,31 @@
<property name="ignoreUnusedRegexp" value="/^_/"/>
</properties>
</rule>

<!-- PHP test fixtures legitimately use direct filesystem calls (creating and
tearing down temporary cache trees); WP_Filesystem is not available or
appropriate in that context. -->
<rule ref="WordPress.WP.AlternativeFunctions">
<exclude-pattern>/tests/php/*</exclude-pattern>
</rule>

<!--
Relocated procedural includes (#1061): these inc/ files are verbatim,
byte-for-byte moves of legacy function clusters out of wp-cache.php. The
pure relocation intentionally makes no behaviour change, so the pre-existing
WPCS debt in wp-cache.php travels with the code unchanged. wp-cache.php
itself carries the same debt and is only ever lint-checked on changed lines;
excluding these relocations keeps that de-facto treatment. WPCS
modernization of the relocated files is a tracked follow-up, out of scope
for the move. Newly authored inc/ files (e.g. inc/boost.php) are NOT listed
here and remain fully linted.
-->
<exclude-pattern>/inc/plugins-cookies\.php</exclude-pattern>
<exclude-pattern>/inc/cache-files\.php</exclude-pattern>
<exclude-pattern>/inc/htaccess\.php</exclude-pattern>
<exclude-pattern>/inc/settings-forms\.php</exclude-pattern>
<exclude-pattern>/inc/preload\.php</exclude-pattern>
<exclude-pattern>/inc/lifecycle\.php</exclude-pattern>
<exclude-pattern>/inc/admin-notices\.php</exclude-pattern>
<exclude-pattern>/inc/admin-ui\.php</exclude-pattern>
</ruleset>
62 changes: 62 additions & 0 deletions CONTEXT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# WP Super Cache — Context

WP Super Cache is a procedural WordPress caching plugin. Most code is global
functions (no namespaces); `src/` is reserved for classmap-autoloaded classes,
and `inc/` holds the global-function include files.

## Entry points

- **`wp-cache.php`** — the plugin main file, reduced to a thin loader: plugin
header, constants, the `require_once` include list, `wpsc_init()` and its init
wiring, the frontend `wp_die`/`set_home` hooks, and the
activation/deactivation/uninstall registrations (which must stay here so
`__FILE__` resolves to the main plugin file).
- **`wp-cache-phase2.php`** — the drop-in caching engine on the request hot path
(served via `advanced-cache.php`). Not part of the admin/lifecycle split.

## `inc/` file map (admin & lifecycle clusters)

Each file owns one responsibility plus its own `add_action`/`add_filter`
registrations. Admin-only function clusters live in dedicated `inc/` files.

| File | Responsibility |
|------|----------------|
| `inc/boost.php` | Jetpack Boost migration notice/AJAX and the install banner |
| `inc/plugins-cookies.php` | WPSC plugin-list & extra-cookie management; feed GC |
| `inc/cache-files.php` | Admin cache-file listing, stats, sizing, cleaning; per-post invalidation |
| `inc/htaccess.php` | `.htaccess` / mod_rewrite rule generation and management |
| `inc/settings-forms.php` | Settings-page form handlers and validators |
| `inc/preload.php` | The preload subsystem (status, cron worker, scheduling, UI, AJAX) |
| `inc/lifecycle.php` | Activation/deactivation/uninstall, enable/disable, advanced-cache + config/cache-dir management, GC defaults |
| `inc/admin-notices.php` | Admin notices, plugin-row UI, the front-page site check |
| `inc/admin-ui.php` | The settings screen: menus, enqueues, the renderer (incl. inline JS), tabs, render helpers |
| `inc/delete-cache-button.php`, `inc/preload-notification.php` | Pre-existing standalone admin features |

The `inc/` files are loaded by the `require_once` block at the top of
`wp-cache.php`, which runs before `wpsc_init()`, so relocated functions are
defined in time for their hooks.

### Relocated-code conventions

- Functions in the relocated clusters keep their original global names and
signatures — themes/plugins calling them see no change.
- `__DIR__` / `__FILE__` inside relocated code resolves to the plugin root via
`dirname( __DIR__ )` (the `inc/` files sit one level below root).
- The relocated procedural includes carry pre-existing WPCS debt verbatim and
are excluded from PHPCS in `.phpcs.xml.dist`; modernizing them is a tracked
follow-up, separate from the relocation. Newly authored `inc/` files remain
fully linted.

## Tests

Two tiers (see `tests/php/README.md`):

- **CI smoke** — `composer test-php`, PHPUnit 9.6, no database; covers
WordPress-free helpers in `wp-cache-phase2.php`.
- **Local integration** — `make test-integration`, `WP_UnitTestCase` + a real
database in the wp-env Docker environment; the bootstrap loads `wp-cache.php`
so the relocated procedural functions run under a real WP runtime.
- **e2e** — `composer test-e2e` (Docker/Jest); covers activation and the
settings UI, including behaviour that depends on plugin-root path resolution.

See `docs/adr/` for the decisions behind this layout.
61 changes: 61 additions & 0 deletions docs/adr/0001-split-wp-cache-into-inc-files.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# 1. Split wp-cache.php into per-responsibility inc/ files

Date: 2026-06-17

## Status

Accepted

## Context

`wp-cache.php` had grown to ~4,500 lines holding ~120 global functions with
their hook registrations interleaved inline — lifecycle, admin UI, preload,
htaccess generation, cache-file management, settings forms, and Jetpack Boost
banners all in one file, changing at different rates. Navigating or safely
editing it was slow, and there was no PHP unit/integration coverage.

Issue #1061 (executing #1047 item 4) called for splitting the file along its
responsibilities, preceded by a characterization test net, as a **pure
relocation** with zero behaviour change.

## Decision

Move the global-function clusters into ~9 focused `inc/` files (the existing
convention for non-class includes; `src/` stays reserved for classmap-autoloaded
classes), one responsibility per file, each carrying its own
`add_action`/`add_filter` registrations. `wp-cache.php` becomes a thin loader.
The file map is documented in `CONTEXT.md`.

Constraints held: function names, signatures, hook names, and load order are
unchanged; `wp-cache-phase2.php` (the hot-path engine) is untouched; no classes,
shims, or global-state reduction. Each cluster was characterization-tested before
it moved.

A few things are position-dependent and therefore deviate from a literal
byte-for-byte move, while preserving behaviour:

- `register_activation_hook` / `register_deactivation_hook` /
`register_uninstall_hook` stay in `wp-cache.php` so `__FILE__` resolves to the
main plugin file; their handlers live in `inc/lifecycle.php`.
- `__DIR__` / `__FILE__` in relocated code resolves to the plugin root via
`dirname( __DIR__ )` so asset URLs, `WPCACHEHOME`, `advanced-cache.php`
generation, and partial-template paths stay correct.

## Consequences

- The plugin's behaviour is unchanged: the CI smoke suite (34), the local
integration suite (37), and the e2e suite (28) all pass. The e2e suite proved
essential — it caught the `__FILE__`/`__DIR__` path regressions that the
unit/integration tiers do not exercise.
- Each `inc/` file is small and focused, so future changes touch one
responsibility at a time.
- The relocated procedural includes carry their pre-existing WPCS debt verbatim
and are excluded from PHPCS (documented in `.phpcs.xml.dist`). **Follow-up:**
modernize the relocated `inc/` files (strict comparisons, escaping, spacing)
and remove the exclusions; a deeper pass can then reduce global state (e.g.
turn `inc/preload.php` into the #5 preload state machine).
- Because the changed-lines linter (`phpcs-changed`) diffs the cumulative branch
against trunk, a partially-moved tree can make `git` attribute surviving
legacy as "added." Only the original and the fully-moved states are
lint-clean, so this work landed as a single relocation commit rather than one
commit per cluster.
203 changes: 203 additions & 0 deletions inc/admin-notices.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
<?php
/**
* Admin notices and plugin-row UI: index.html security notice, config-file
* notices, the comment-form lockdown message, plugin-row notice / action links /
* favorite action, the caching-broken admin notice, and the front-page site
* check.
*
* Relocated from wp-cache.php (issue #1061) as a pure move: identical function
* signatures and hook registrations, no behaviour change.
*
* @package WP_Super_Cache
*/

function comment_form_lockdown_message() {
?><p><?php _e( "Comment moderation is enabled. Your comment may take some time to appear.", 'wp-super-cache' ); ?></p><?php
}
if( defined( 'WPLOCKDOWN' ) && constant( 'WPLOCKDOWN' ) )
add_action( 'comment_form', 'comment_form_lockdown_message' );

function wp_cache_index_notice() {
global $cache_path;

if ( false == wpsupercache_site_admin() )
return false;
if ( false == get_site_option( 'wp_super_cache_index_detected' ) )
return false;

if ( strlen( $cache_path ) < strlen( ABSPATH )
|| ABSPATH != substr( $cache_path, 0, strlen( ABSPATH ) ) )
return false; // cache stored outside web root

if ( get_site_option( 'wp_super_cache_index_detected' ) == 2 ) {
update_site_option( 'wp_super_cache_index_detected', 3 );
echo "<div class='error' style='padding: 10px 10px 50px 10px'>";
echo "<h2>" . __( 'WP Super Cache Warning!', 'wp-super-cache' ) . '</h2>';
echo '<p>' . __( 'All users of this site have been logged out to refresh their login cookies.', 'wp-super-cache' ) . '</p>';
echo '</div>';
return false;
} elseif ( get_site_option( 'wp_super_cache_index_detected' ) != 3 ) {
echo "<div id='wpsc-index-warning' class='error notice' style='padding: 10px 10px 50px 10px'>";
echo "<h2>" . __( 'WP Super Cache Warning!', 'wp-super-cache' ) . '</h2>';
echo '<p>' . __( 'Your server is configured to show files and directories, which may expose sensitive data such as login cookies to attackers in the cache directories. That has been fixed by adding a file named index.html to each directory. If you use simple caching, consider moving the location of the cache directory on the Advanced Settings page.', 'wp-super-cache' ) . '</p>';
echo "<p><strong>";
_e( 'If you just installed WP Super Cache for the first time, you can dismiss this message. Otherwise, you should probably refresh the login cookies of all logged in WordPress users here by clicking the logout link below.', 'wp-super-cache' );
echo "</strong></p>";
echo '<p>' . esc_html__( 'The logout link will log out all WordPress users on this site except you. Your authentication cookie will be updated, but you will not be logged out.', 'wp-super-cache' ) . '</p>';
echo '<a id="wpsc-dismiss" href="#">' . esc_html__( 'Dismiss', 'wp-super-cache' ) . '</a>';
echo ' | <a href="' . esc_url( wp_nonce_url( admin_url( '?action=wpsclogout' ), 'wpsc_logout' ) ) . '">' . esc_html__( 'Logout', 'wp-super-cache' ) . '</a>';
echo '</div>';
?>
<script type='text/javascript'>
<!--
jQuery(document).ready(function(){
jQuery('#wpsc-dismiss').on("click",function() {
jQuery.ajax({
type: "post",url: "admin-ajax.php",data: { action: 'wpsc-index-dismiss', _ajax_nonce: '<?php echo wp_create_nonce( 'wpsc-index-dismiss' ); ?>' },
beforeSend: function() {jQuery("#wpsc-index-warning").fadeOut('slow');},
});
})
})
//-->
</script>
<?php
}
}
add_action( 'admin_notices', 'wp_cache_index_notice' );

function wpsc_config_file_notices() {
global $wp_cache_config_file;
if ( ! isset( $_GET['page'] ) || $_GET['page'] != 'wpsupercache' ) {
return false;
}
$notice = get_transient( 'wpsc_config_error' );
if ( ! $notice ) {
return false;
}
switch( $notice ) {
case 'error_move_tmp_config_file':
$msg = sprintf( __( 'Error: Could not rename temporary file to configuration file. Please make sure %s is writeable by the webserver.' ), $wp_cache_config_file );
break;
case 'config_file_ro':
$msg = sprintf( __( 'Error: Configuration file is read only. Please make sure %s is writeable by the webserver.' ), $wp_cache_config_file );
break;
case 'tmp_file_ro':
$msg = sprintf( __( 'Error: The directory containing the configuration file %s is read only. Please make sure it is writeable by the webserver.' ), $wp_cache_config_file );
break;
case 'config_file_not_loaded':
$msg = sprintf( __( 'Error: Configuration file %s could not be loaded. Please reload the page.' ), $wp_cache_config_file );
break;
case 'config_file_missing':
$msg = sprintf( __( 'Error: Configuration file %s is missing. Please reload the page.' ), $wp_cache_config_file );
break;

}
echo '<div class="error"><p><strong>' . $msg . '</strong></p></div>';
}
add_action( 'admin_notices', 'wpsc_config_file_notices' );
function wpsc_dismiss_indexhtml_warning() {
check_ajax_referer( 'wpsc-index-dismiss' );

if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error( null, 403 );
}

update_site_option( 'wp_super_cache_index_detected', 3 );
die( 0 );
}
add_action( 'wp_ajax_wpsc-index-dismiss', 'wpsc_dismiss_indexhtml_warning' );

if( get_option( 'gzipcompression' ) )
update_option( 'gzipcompression', 0 );

function wp_cache_favorite_action( $actions ) {
if ( function_exists( '_deprecated_function' ) ) {
_deprecated_function( __FUNCTION__, 'WP Super Cache 1.6.4' );
}

if ( false == wpsupercache_site_admin() )
return $actions;

if ( function_exists('current_user_can') && !current_user_can('manage_options') )
return $actions;

$actions[ wp_nonce_url( 'options-general.php?page=wpsupercache&wp_delete_cache=1&tab=contents', 'wp-cache' ) ] = array( __( 'Delete Cache', 'wp-super-cache' ), 'manage_options' );

return $actions;
}
//add_filter( 'favorite_actions', 'wp_cache_favorite_action' );

function wp_cache_plugin_notice( $plugin ) {
global $cache_enabled;
if( $plugin == 'wp-super-cache/wp-cache.php' && !$cache_enabled && function_exists( 'admin_url' ) )
echo '<td colspan="5" class="plugin-update">' . sprintf( __( 'WP Super Cache must be configured. Go to <a href="%s">the admin page</a> to enable and configure the plugin.', 'wp-super-cache' ), admin_url( 'options-general.php?page=wpsupercache' ) ) . '</td>';
}
add_action( 'after_plugin_row', 'wp_cache_plugin_notice' );

function wp_cache_plugin_actions( $links, $file ) {
if ( $file === 'wp-super-cache/wp-cache.php' && function_exists( 'admin_url' ) && is_array( $links ) ) {
$settings_link = '<a href="' . admin_url( 'options-general.php?page=wpsupercache' ) . '">' . __( 'Settings', 'wp-super-cache' ) . '</a>';
array_unshift( $links, $settings_link ); // before other links
}
return $links;
}
add_filter( 'plugin_action_links', 'wp_cache_plugin_actions', 10, 2 );

function wp_cache_admin_notice() {
global $cache_enabled, $wp_cache_phase1_loaded;
if( substr( $_SERVER['PHP_SELF'], -11 ) == 'plugins.php' && !$cache_enabled && function_exists( 'admin_url' ) )
echo '<div class="notice notice-info"><p><strong>' . sprintf( __('WP Super Cache is disabled. Please go to the <a href="%s">plugin admin page</a> to enable caching.', 'wp-super-cache' ), admin_url( 'options-general.php?page=wpsupercache' ) ) . '</strong></p></div>';

if ( defined( 'WP_CACHE' ) && WP_CACHE == true && ( defined( 'ADVANCEDCACHEPROBLEM' ) || ( $cache_enabled && false == isset( $wp_cache_phase1_loaded ) ) ) ) {
if ( wp_cache_create_advanced_cache() ) {
echo '<div class="notice notice-error"><p>' . sprintf( __( 'Warning! WP Super Cache caching <strong>was</strong> broken but has been <strong>fixed</strong>! The script advanced-cache.php could not load wp-cache-phase1.php.<br /><br />The file %1$s/advanced-cache.php has been recreated and WPCACHEHOME fixed in your wp-config.php. Reload to hide this message.', 'wp-super-cache' ), WP_CONTENT_DIR ) . '</p></div>';
}
}
}
add_action( 'admin_notices', 'wp_cache_admin_notice' );

function wp_cache_check_site() {
global $wp_super_cache_front_page_check, $wp_super_cache_front_page_clear, $wp_super_cache_front_page_text, $wp_super_cache_front_page_notification, $wpdb;

if ( empty( $wp_super_cache_front_page_check ) ) {
return false;
}

if ( function_exists( "wp_remote_get" ) == false ) {
return false;
}
$front_page = wp_remote_get( site_url(), array('timeout' => 60, 'blocking' => true ) );
if( is_array( $front_page ) ) {
// Check for gzipped front page
if ( $front_page[ 'headers' ][ 'content-type' ] == 'application/x-gzip' ) {
if ( ! isset( $wp_super_cache_front_page_clear ) || $wp_super_cache_front_page_clear === 0 ) {
wp_mail( get_option( 'admin_email' ), sprintf( __( '[%s] Front page is gzipped! Please clear cache!', 'wp-super-cache' ), home_url() ), sprintf( __( "Please visit %s to clear the cache as the front page of your site is now downloading!", 'wp-super-cache' ), admin_url( 'options-general.php?page=wpsupercache' ) ) );
} else {
wp_cache_clear_cache( $wpdb->blogid );
wp_mail( get_option( 'admin_email' ), sprintf( __( '[%s] Front page is gzipped! Cache Cleared!', 'wp-super-cache' ), home_url() ), sprintf( __( "The cache on your blog has been cleared because the front page of your site is now downloading. Please visit %s to verify the cache has been cleared.", 'wp-super-cache' ), admin_url( 'options-general.php?page=wpsupercache' ) ) );
}
}

// Check for broken front page
if (
! empty( $wp_super_cache_front_page_text )
&& ! str_contains( $front_page['body'], $wp_super_cache_front_page_text )
) {
if ( ! isset( $wp_super_cache_front_page_clear ) || $wp_super_cache_front_page_clear === 0 ) {
wp_mail( get_option( 'admin_email' ), sprintf( __( '[%s] Front page is not correct! Please clear cache!', 'wp-super-cache' ), home_url() ), sprintf( __( 'Please visit %1$s to clear the cache as the front page of your site is not correct and missing the text, "%2$s"!', 'wp-super-cache' ), admin_url( 'options-general.php?page=wpsupercache' ), $wp_super_cache_front_page_text ) );
} else {
wp_cache_clear_cache( $wpdb->blogid );
wp_mail( get_option( 'admin_email' ), sprintf( __( '[%s] Front page is not correct! Cache Cleared!', 'wp-super-cache' ), home_url() ), sprintf( __( 'The cache on your blog has been cleared because the front page of your site is missing the text "%2$s". Please visit %1$s to verify the cache has been cleared.', 'wp-super-cache' ), admin_url( 'options-general.php?page=wpsupercache' ), $wp_super_cache_front_page_text ) );
}
}
}
if ( isset( $wp_super_cache_front_page_notification ) && $wp_super_cache_front_page_notification == 1 ) {
wp_mail( get_option( 'admin_email' ), sprintf( __( '[%s] Front page check!', 'wp-super-cache' ), home_url() ), sprintf( __( "WP Super Cache has checked the front page of your blog. Please visit %s if you would like to disable this.", 'wp-super-cache' ) . "\n\n", admin_url( 'options-general.php?page=wpsupercache' ) ) );
}

if ( !wp_next_scheduled( 'wp_cache_check_site_hook' ) ) {
wp_schedule_single_event( time() + 360 , 'wp_cache_check_site_hook' );
wp_cache_debug( 'scheduled wp_cache_check_site_hook for 360 seconds time.', 2 );
}
}
add_action( 'wp_cache_check_site_hook', 'wp_cache_check_site' );
Loading
Loading