Skip to content
Open
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
44 changes: 43 additions & 1 deletion inc/manager.php
Original file line number Diff line number Diff line change
Expand Up @@ -430,6 +430,48 @@ public function register_after_setup() {
public static function should_load_profiler( $default_value = false ) {
return ! $default_value && apply_filters( 'optml_page_profiler_disable', false ) === false;
}

/**
* Decide if the temporary Cache-Control header can be sent while page profiling is pending.
*
* The header must never be sent on non-cacheable pages: it would replace a
* Cache-Control header already set by WordPress or another plugin (PHP's
* header() replaces same-name headers by default), e.g. WooCommerce's
* no-cache header on cart and checkout, letting proxies cache user-specific
* pages. We back off when DONOTCACHEPAGE is set or when any Cache-Control
* header exists already, and let developers override the decision.
*
* @param array<int, string>|null $sent_headers Headers already set for the response; defaults to headers_list().
* @param bool|null $do_not_cache Whether the page is flagged as non-cacheable; defaults to the DONOTCACHEPAGE constant.
*
* @return bool Whether the header can be sent.
*/
public function should_send_temporary_cache_header( $sent_headers = null, $do_not_cache = null ) {
if ( null === $do_not_cache ) {
$do_not_cache = defined( 'DONOTCACHEPAGE' ) && DONOTCACHEPAGE;
}
$send = ! $do_not_cache;

if ( $send ) {
if ( null === $sent_headers ) {
$sent_headers = headers_list();
}
foreach ( $sent_headers as $header ) {
if ( stripos( $header, 'cache-control:' ) === 0 ) {
$send = false;
break;
}
}
}

/**
* Filters whether the temporary `Cache-Control: max-age=300` header is sent
* while page profiling is pending for the current page.
*
* @param bool $send Computed decision: false when DONOTCACHEPAGE is set or a Cache-Control header exists already.
*/
return apply_filters( 'optml_send_temporary_cache_header', $send ) === true;
}
/**
* Filter raw HTML content for urls.
*
Expand Down Expand Up @@ -461,7 +503,7 @@ public function replace_content( $html, $partial = false ) {
$js_optimizer
);
$html = str_replace( Optml_Admin::get_optimizer_script( true ), $js_optimizer, $html );
if ( ! headers_sent() ) {
if ( ! headers_sent() && $this->should_send_temporary_cache_header() ) {
header( 'Cache-Control: max-age=300' ); // Attempt to cache the page just for 5 mins until the optimizer is done. Once the optimizer is done, the page will load optimized.
}
} else {
Expand Down
98 changes: 98 additions & 0 deletions tests/test-cache-header.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
<?php
/**
* WordPress unit tests for the temporary Cache-Control header decision.
*
* @package Optimole-WP
* @subpackage Tests
* @copyright Copyright (c) 2026, ThemeIsle
* @license http://opensource.org/licenses/gpl-2.0.php GNU Public License
*/

/**
* Class Test_Cache_Header.
*/
class Test_Cache_Header extends WP_UnitTestCase {

/**
* With no signals against it, the header is sent.
*/
public function test_sends_by_default() {
$manager = Optml_Manager::instance();
$this->assertTrue( $manager->should_send_temporary_cache_header( [], false ) );
}

/**
* Unrelated headers do not block it.
*/
public function test_unrelated_headers_do_not_block() {
$manager = Optml_Manager::instance();
$headers = [
'Content-Type: text/html; charset=UTF-8',
'X-Pingback: http://example.org/xmlrpc.php',
'Pragma: no-cache',
];
$this->assertTrue( $manager->should_send_temporary_cache_header( $headers, false ) );
}

/**
* An existing Cache-Control header is never overridden, e.g. WooCommerce's
* nocache_headers() output on cart and checkout pages.
*/
public function test_existing_cache_control_blocks() {
$manager = Optml_Manager::instance();
$headers = [
'Expires: Wed, 11 Jan 1984 05:00:00 GMT',
'Cache-Control: no-cache, must-revalidate, max-age=0',
];
$this->assertFalse( $manager->should_send_temporary_cache_header( $headers, false ) );
}

/**
* The Cache-Control match is case-insensitive and value-agnostic — a longer
* max-age set by a cache plugin must not be downgraded either.
*/
public function test_existing_cache_control_case_and_value_agnostic() {
$manager = Optml_Manager::instance();
$this->assertFalse( $manager->should_send_temporary_cache_header( [ 'cache-control: public, max-age=31536000' ], false ) );
}

/**
* DONOTCACHEPAGE blocks the header.
*/
public function test_donotcachepage_blocks() {
$manager = Optml_Manager::instance();
$this->assertFalse( $manager->should_send_temporary_cache_header( [], true ) );
}

/**
* The filter can force the header off.
*/
public function test_filter_can_disable() {
add_filter( 'optml_send_temporary_cache_header', '__return_false' );
$manager = Optml_Manager::instance();
$this->assertFalse( $manager->should_send_temporary_cache_header( [], false ) );
}

/**
* The filter can force the header on despite blocking signals.
*/
public function test_filter_can_force_enable() {
add_filter( 'optml_send_temporary_cache_header', '__return_true' );
$manager = Optml_Manager::instance();
$this->assertTrue( $manager->should_send_temporary_cache_header( [ 'Cache-Control: no-cache' ], true ) );
}

/**
* A non-boolean filter return does not accidentally enable the header.
*/
public function test_non_boolean_filter_return_is_not_true() {
add_filter(
'optml_send_temporary_cache_header',
function () {
return 'yes';
}
);
$manager = Optml_Manager::instance();
$this->assertFalse( $manager->should_send_temporary_cache_header( [], false ) );
}
}
Loading