PluginProbe
xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN / 1.4.2
xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN v1.4.2
1.4.2 1.4.1 1.4.0 1.3.7 1.3.6 1.3.5 1.3.4 1.3.3 1.3.2 1.3.1 1.3.0 1.2.4 trunk 1.0.0 1.0.1 1.0.2 1.0.3 1.0.4 1.0.5 1.0.6 1.0.7 1.0.8 1.0.9 1.1.0 1.1.1 All 36 releases
xspeed / includes / class-cache-off-notice.php

class-cache-off-notice.php in xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN 1.4.2, at includes/class-cache-off-notice.php

188 lines 8.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
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