| 1 |
<?php |
| 2 |
/** |
| 3 |
* A notice on every admin page while the page cache is off (#594). |
| 4 |
* |
| 5 |
* The page cache defaults to off, and only the setup wizard turns it on. A |
| 6 |
* site activated with WP-CLI, MCP or a host's bulk install, or where nobody |
| 7 |
* finished the wizard, sent every visitor an uncached page and wp-admin never |
| 8 |
* said so. The cache is not turned on at activation: a host with its own page |
| 9 |
* cache needs a person to decide. So the notice asks, with a button that |
| 10 |
* turns it on. Dismissing it is kept for the site, so an admin who turned the |
| 11 |
* cache off on purpose sees it once. |
| 12 |
* |
| 13 |
* @package XSpeed |
| 14 |
*/ |
| 15 |
|
| 16 |
declare(strict_types=1); |
| 17 |
|
| 18 |
namespace XSpeed; |
| 19 |
|
| 20 |
defined( 'ABSPATH' ) || exit; |
| 21 |
|
| 22 |
final class Cache_Off_Notice { |
| 23 |
|
| 24 |
/** admin-post action of the "Turn it on" button. */ |
| 25 |
public const ACTION_ON = 'xspeed_cache_off_notice_on'; |
| 26 |
|
| 27 |
/** admin-post action of the Dismiss link. */ |
| 28 |
public const ACTION_DISMISS = 'xspeed_cache_off_notice_dismiss'; |
| 29 |
|
| 30 |
/** Set when an admin dismissed the notice. Kept for the site. */ |
| 31 |
public const OPT_DISMISSED = 'xspeed_cache_off_notice_dismissed'; |
| 32 |
|
| 33 |
/** Per-user transient prefix carrying a refused "Turn it on" across the redirect. */ |
| 34 |
private const REFUSED_PREFIX = 'xspeed_cache_on_refused_'; |
| 35 |
|
| 36 |
/** Query arg carrying the dashboard route (the URL fragment) back through the redirect. */ |
| 37 |
private const ROUTE_PARAM = 'xspeed_route'; |
| 38 |
|
| 39 |
/** Printed already: it is hooked to two notice actions. */ |
| 40 |
private static bool $shown = false; |
| 41 |
|
| 42 |
public static function boot(): void { |
| 43 |
// xSpeed's own screens strip `admin_notices` and relay their own |
| 44 |
// through `xspeed_admin_notices` (Admin::suppress_foreign_notices()). |
| 45 |
add_action( 'admin_notices', array( __CLASS__, 'maybe_notice' ) ); |
| 46 |
add_action( 'xspeed_admin_notices', array( __CLASS__, 'maybe_notice' ) ); |
| 47 |
add_action( 'admin_post_' . self::ACTION_ON, array( __CLASS__, 'handle_on' ) ); |
| 48 |
add_action( 'admin_post_' . self::ACTION_DISMISS, array( __CLASS__, 'handle_dismiss' ) ); |
| 49 |
} |
| 50 |
|
| 51 |
/** Whether the notice applies to this site and user now. */ |
| 52 |
public static function applies(): bool { |
| 53 |
if ( ! current_user_can( 'manage_options' ) || ( function_exists( 'is_network_admin' ) && is_network_admin() ) ) { |
| 54 |
return false; |
| 55 |
} |
| 56 |
$opts = get_option( 'xspeed_options', array() ); |
| 57 |
if ( is_array( $opts ) && ! empty( $opts['cache_enabled'] ) ) { |
| 58 |
return false; |
| 59 |
} |
| 60 |
if ( get_option( self::OPT_DISMISSED ) ) { |
| 61 |
return false; |
| 62 |
} |
| 63 |
// Another cache plugin is serving pages. "Built from scratch" would be |
| 64 |
// false, and the site chose that cache: the dashboard still offers the |
| 65 |
// takeover, but every admin screen should not. |
| 66 |
if ( in_array( |
| 67 |
Page_Cache_Detector::classify()['state'], |
| 68 |
array( Page_Cache_Detector::STATE_FOREIGN_LIVE, Page_Cache_Detector::STATE_POSSIBLE_LIVE, Page_Cache_Detector::STATE_CONTESTED ), |
| 69 |
true |
| 70 |
) ) { |
| 71 |
return false; |
| 72 |
} |
| 73 |
// The setup wizard asks the same question, with the cache on by default. |
| 74 |
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- screen check only. |
| 75 |
$page = isset( $_GET['page'] ) ? sanitize_key( wp_unslash( (string) $_GET['page'] ) ) : ''; |
| 76 |
return Onboarding::PAGE_SLUG !== $page; |
| 77 |
} |
| 78 |
|
| 79 |
public static function maybe_notice(): void { |
| 80 |
if ( self::$shown ) { |
| 81 |
return; |
| 82 |
} |
| 83 |
$user = get_current_user_id(); |
| 84 |
$refused = get_transient( self::REFUSED_PREFIX . $user ); |
| 85 |
if ( is_string( $refused ) && '' !== $refused ) { |
| 86 |
delete_transient( self::REFUSED_PREFIX . $user ); |
| 87 |
self::$shown = true; |
| 88 |
printf( |
| 89 |
'<div class="notice notice-error"><p><strong>%1$s</strong> %2$s</p></div>', |
| 90 |
esc_html__( 'xSpeed could not turn the page cache on.', 'xspeed' ), |
| 91 |
esc_html( $refused ) |
| 92 |
); |
| 93 |
return; |
| 94 |
} |
| 95 |
if ( ! self::applies() ) { |
| 96 |
return; |
| 97 |
} |
| 98 |
self::$shown = true; |
| 99 |
|
| 100 |
// Turning the cache on replaces any advanced-cache.php already there. |
| 101 |
// The dashboard names its owner before that happens (QA on #642: one |
| 102 |
// click here took over WP Super Cache's cache without a word), so the |
| 103 |
// notice does too, and only its takeover link counts as consent. |
| 104 |
$disclosure = Page_Cache_Detector::dropin_disclosure(); |
| 105 |
$on_url = admin_url( 'admin-post.php?action=' . self::ACTION_ON ); |
| 106 |
if ( $disclosure['replaceable'] ) { |
| 107 |
$on_url = add_query_arg( 'takeover', '1', $on_url ); |
| 108 |
if ( null !== $disclosure['label'] ) { |
| 109 |
$body = sprintf( |
| 110 |
/* translators: %s: the page-caching plugin that owns advanced-cache.php. */ |
| 111 |
__( 'Every visitor gets a page built from scratch. %s owns the page-cache file now. Turning xSpeed\'s cache on replaces it, and xSpeed serves the cached pages instead.', 'xspeed' ), |
| 112 |
$disclosure['label'] |
| 113 |
); |
| 114 |
/* translators: %s: the page-caching plugin that owns advanced-cache.php. */ |
| 115 |
$button = sprintf( __( 'Take over from %s', 'xspeed' ), $disclosure['label'] ); |
| 116 |
} else { |
| 117 |
$body = __( 'Every visitor gets a page built from scratch. Another page-cache file is in wp-content now. Turning xSpeed\'s cache on replaces it.', 'xspeed' ); |
| 118 |
$button = __( 'Replace it and turn on', 'xspeed' ); |
| 119 |
} |
| 120 |
} else { |
| 121 |
$body = __( 'Every visitor gets a page built from scratch. Turn it on unless your host already caches pages for this site.', 'xspeed' ); |
| 122 |
$button = __( 'Turn it on', 'xspeed' ); |
| 123 |
} |
| 124 |
|
| 125 |
printf( |
| 126 |
'<div id="xspeed-cache-off-notice" class="notice notice-warning"><p><strong>%1$s</strong> %2$s</p><p><a class="button button-primary" data-xspeed-keep-route href="%3$s">%4$s</a> <a class="button" href="%5$s">%6$s</a> <a data-xspeed-keep-route href="%7$s">%8$s</a></p></div>', |
| 127 |
esc_html__( "xSpeed's page cache is off.", 'xspeed' ), |
| 128 |
esc_html( $body ), |
| 129 |
esc_url( wp_nonce_url( $on_url, self::ACTION_ON ) ), |
| 130 |
esc_html( $button ), |
| 131 |
esc_url( admin_url( 'admin.php?page=' . Admin::PAGE_SLUG . '#/cache/cache' ) ), |
| 132 |
esc_html__( 'Page cache settings', 'xspeed' ), |
| 133 |
esc_url( wp_nonce_url( admin_url( 'admin-post.php?action=' . self::ACTION_DISMISS ), self::ACTION_DISMISS ) ), |
| 134 |
esc_html__( 'Dismiss', 'xspeed' ) |
| 135 |
); |
| 136 |
// The dashboard keeps its screen in the URL fragment, which no |
| 137 |
// referer carries, so the redirect back landed on Overview. The |
| 138 |
// links pass the fragment along when clicked. |
| 139 |
echo "<script>document.querySelectorAll('#xspeed-cache-off-notice [data-xspeed-keep-route]').forEach(function(a){a.addEventListener('click',function(){if(location.hash){var u=new URL(a.href);u.searchParams.set('" . esc_js( self::ROUTE_PARAM ) . "',location.hash);a.href=u.toString();}});});</script>"; |
| 140 |
} |
| 141 |
|
| 142 |
/** |
| 143 |
* "Turn it on": the same Cache::toggle() the dashboard switch calls, so a |
| 144 |
* drop-in or WP_CACHE line owned by another plugin is refused the same |
| 145 |
* way. A refusal is shown on the next page instead of the notice. |
| 146 |
* |
| 147 |
* Another plugin's drop-in is taken over only from the link that named it. |
| 148 |
* If one appeared after the notice was printed, the toggle refuses and |
| 149 |
* says whose it is. |
| 150 |
*/ |
| 151 |
public static function handle_on(): void { |
| 152 |
self::authorize( self::ACTION_ON ); |
| 153 |
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- authorize() checked the nonce. |
| 154 |
$takeover = isset( $_GET['takeover'] ) && '1' === $_GET['takeover']; |
| 155 |
$state = Cache::toggle( true, $takeover ); |
| 156 |
if ( ! empty( $state['blocked'] ) ) { |
| 157 |
$reason = isset( $state['blocked_reason'] ) && is_string( $state['blocked_reason'] ) ? $state['blocked_reason'] : ''; |
| 158 |
set_transient( self::REFUSED_PREFIX . get_current_user_id(), '' !== $reason ? $reason : __( 'Open the page cache settings to see why.', 'xspeed' ), 5 * MINUTE_IN_SECONDS ); |
| 159 |
} |
| 160 |
self::back(); |
| 161 |
} |
| 162 |
|
| 163 |
public static function handle_dismiss(): void { |
| 164 |
self::authorize( self::ACTION_DISMISS ); |
| 165 |
update_option( self::OPT_DISMISSED, time(), false ); |
| 166 |
self::back(); |
| 167 |
} |
| 168 |
|
| 169 |
private static function authorize( string $action ): void { |
| 170 |
if ( ! current_user_can( 'manage_options' ) ) { |
| 171 |
wp_die( esc_html__( 'You are not allowed to do that.', 'xspeed' ), '', array( 'response' => 403 ) ); |
| 172 |
} |
| 173 |
check_admin_referer( $action ); |
| 174 |
} |
| 175 |
|
| 176 |
private static function back(): void { |
| 177 |
$referer = wp_get_referer(); |
| 178 |
$target = $referer ? $referer : admin_url(); |
| 179 |
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- authorize() checked the nonce. |
| 180 |
$route = isset( $_GET[ self::ROUTE_PARAM ] ) ? sanitize_text_field( wp_unslash( (string) $_GET[ self::ROUTE_PARAM ] ) ) : ''; |
| 181 |
if ( false === strpos( $target, '#' ) && preg_match( '~^#/[A-Za-z0-9/_-]*$~', $route ) ) { |
| 182 |
$target .= $route; |
| 183 |
} |
| 184 |
wp_safe_redirect( $target ); |
| 185 |
exit; |
| 186 |
} |
| 187 |
} |
| 188 |
|