PluginProbe ʕ •ᴥ•ʔ
EmbedPress – PDF Embedder, 3D PDF FlipBook, Google Reviews, YouTube Videos, Upload & Embed PDF documents / 4.6.5
EmbedPress – PDF Embedder, 3D PDF FlipBook, Google Reviews, YouTube Videos, Upload & Embed PDF documents v4.6.5
4.6.5 4.6.4 4.6.3 4.6.2 4.6.1 4.6.0 4.5.6 4.5.5 4.5.4 4.5.3 4.5.2 trunk 1.0.0 1.1.0 1.1.1 1.1.2 1.1.3 1.2.0 1.3.0 1.3.1 1.4.0 1.4.1 1.4.2 1.4.3 1.4.4 1.5.0 1.6.0 1.6.1 1.6.2 1.6.3 1.7.0 1.7.1 1.7.2 1.7.3 1.7.4 1.7.5 2.0.0 2.0.1 2.0.2 2.0.3 2.1.0 2.1.1 2.1.2 2.1.3 2.1.4 2.1.5 2.1.6 2.2.0 2.2.1 2.2.2 2.3.0 2.3.1 2.3.2 2.3.3 2.4.0 2.4.1 2.5.0 2.5.1 2.5.2 2.5.3 2.5.4 2.5.5 2.6.0 2.6.1 2.6.2 2.7.0 2.7.1 2.7.2 2.7.3 2.7.4 2.7.5 2.7.6 2.7.7 3.0.0 3.0.1 3.0.2 3.0.3 3.0.4 3.1.0 3.1.1 3.1.2 3.1.3 3.2.0 3.2.1 3.3.0 3.3.1 3.3.2 3.3.3 3.3.4 3.3.5 3.3.6 3.3.7 3.4.0 3.4.1 3.4.2 3.4.3 3.5.0 3.5.1 3.5.2 3.5.3 3.6.0 3.6.1 3.6.2 3.6.3 3.6.4 3.6.5 3.6.6 3.6.7 3.6.8 3.7.0 3.7.1 3.7.2 3.7.3 3.8.0 3.8.1 3.8.2 3.8.3 3.8.4 3.8.5 3.9.0 3.9.1 3.9.10 3.9.11 3.9.12 3.9.13 3.9.14 3.9.15 3.9.16 3.9.17 3.9.2 3.9.3 3.9.4 3.9.5 3.9.6 3.9.7 3.9.8 3.9.9 4.0.0 4.0.1 4.0.10 4.0.11 4.0.12 4.0.13 4.0.14 4.0.2 4.0.3 4.0.4 4.0.5 4.0.6 4.0.7 4.0.8 4.0.9 4.1.0 4.1.1 4.1.10 4.1.2 4.1.3 4.1.4 4.1.5 4.1.6 4.1.7 4.1.8 4.1.9 4.2.0 4.2.1 4.2.2 4.2.3 4.2.4 4.2.5 4.2.6 4.2.7 4.2.8 4.2.9 4.3.0 4.3.1 4.4.0 4.4.1 4.4.10 4.4.11 4.4.2 4.4.3 4.4.4 4.4.5 4.4.6 4.4.7 4.4.8 4.4.9 4.5.0 4.5.1
embedpress / includes / Vendor / page-cache-safety / class-page-cache-setup.php
embedpress / includes / Vendor / page-cache-safety Last commit date
class-page-cache-safety.php 6 days ago class-page-cache-setup.php 6 days ago
class-page-cache-setup.php
524 lines
1 <?php
2
3 /**
4 * Page Cache Setup — how to bring xSpeed up already configured, for a plugin
5 * that installs it on a user's behalf.
6 *
7 * The companion to class-page-cache-safety.php. That file DECIDES (read-only:
8 * "does anything already own this page cache?"); this one CONFIGURES. Keep the
9 * split — Detector promises never to write, and hosts reason about it on that
10 * basis.
11 *
12 * Call order matters and is enforced in prepare(). See page-cache-safety/README.md.
13 *
14 * Source of truth: the xSpeed Free repo, page-cache-safety/. Not loaded by
15 * xSpeed, not shipped in its zip — it exists to be copied. Improve it here and
16 * re-copy; do not fork it per plugin.
17 *
18 * @package WPDeveloper\PageCacheSafety
19 * @version 1.4.1
20 */
21
22 namespace WPDeveloper\PageCacheSafety;
23
24 defined('ABSPATH') || exit;
25
26 /*
27 * Two plugins on the same site can each carry a copy. First one loaded wins.
28 * Unlike Detector these methods write — but everything they write is xSpeed's
29 * own option rows, to the same values, so whichever copy runs is immaterial.
30 */
31 if (! class_exists(__NAMESPACE__ . '\\Setup')) {
32
33 final class Setup
34 {
35
36 public const VERSION = '1.4.1';
37
38 /**
39 * A host offering this a row on a screen should show the real name and
40 * link: the user is agreeing to install this specific plugin.
41 */
42 public const PLUGIN_FILE = 'xspeed/xspeed.php';
43 public const PLUGIN_SLUG = 'xspeed';
44 public const PLUGIN_NAME = 'xSpeed Cache';
45 public const PLUGIN_ICON = 'https://ps.w.org/xspeed/assets/icon-256x256.png';
46 public const PLUGIN_LINK = 'https://wordpress.org/plugins/xspeed/';
47
48 /**
49 * A host whose own floor is lower must check these before offering: a
50 * promised cache that then fails to install is worse than none offered.
51 */
52 public const REQUIRES_WP = '6.0';
53 public const REQUIRES_PHP = '7.4';
54
55 /** Pre-module blob, and one row per module. */
56 public const SETTINGS_OPTION = 'xspeed_options';
57 public const MODULE_PREFIX = 'xspeed_module_';
58
59 /**
60 * Turns page caching on. Deliberately NOT a module setting — it drives the
61 * drop-in install and the wp-config.php edit, and Settings_Manager rejects
62 * it by name for that reason.
63 */
64 public const PAGE_CACHE_KEY = 'cache_enabled';
65
66 /**
67 * What prepare() overwrote, so rollback() can put it back rather than
68 * deleting rows it never created. Option rows outlive plugin deletion, so
69 * a site with no xSpeed can still have them.
70 */
71 public const SNAPSHOT_OPTION = 'xspeed_setup_snapshot';
72
73 /**
74 * Set unconditionally by xSpeed's activation to force a one-time redirect
75 * to its setup wizard. Nothing a host does before activation can prevent
76 * it, so finish() clears it afterwards.
77 */
78 public const WIZARD_REDIRECT_OPTION = 'xspeed_redirect_to_onboarding';
79
80 /**
81 * The module state a host-installed xSpeed should come up with.
82 *
83 * This is a STATIC COPY of what XSpeed\Settings::conflict_safe_profile()
84 * builds from the live module registry. The copy has to exist: prepare()
85 * runs before activation, when none of xSpeed's code is loaded and there
86 * is no registry to ask. PortableSetupParityTest compares the two and
87 * fails when they drift, and the Pro repo's own test covers its half.
88 *
89 * Two sources feed it, because scanning one misses half the switches:
90 * settings whose schema default is `true` (an ABSENT option row reads
91 * back ON, so these need explicit `false` rows), and the four that
92 * activation seeds through Settings::recommended_module_settings().
93 *
94 * Page caching only. Someone who accepted "install a cache" agreed to a page
95 * cache, not to having their markup rewritten by minify, lazy loading or
96 * resource hints.
97 *
98 * Every module is listed, including ones already off by default, because "off
99 * by default" is not "off": writing SETTINGS_OPTION suppresses the first-run
100 * path that seeds these rows, so a few land off as a side effect of an
101 * unrelated write. Relying on that silently re-enabled browser caching for one
102 * host already.
103 *
104 * ai-privacy, cloudflare, object-cache and cache keep xSpeed's own defaults —
105 * none is a rendering optimisation, and for the first, off would be wrong.
106 */
107 public const INITIAL_SETTINGS = array(
108 'bloat' => array(
109 'disable_dashicons_frontend' => false,
110 'disable_oembed' => false,
111 'disable_rss_feeds' => false,
112 'disable_xmlrpc' => false,
113 'restrict_rest_to_authed' => false,
114 'strip_jquery_migrate' => false,
115 ),
116 'browser-cache' => array(
117 'enabled' => false,
118 ),
119 'cache' => array(
120 'mobile_separate' => false,
121 ),
122 'cdn' => array(
123 'enabled' => false,
124 ),
125 'cloudflare' => array(
126 'enabled' => false,
127 ),
128 'fonts' => array(
129 'font_display_swap' => false,
130 ),
131 'gzip' => array(
132 'gzip_enabled' => false,
133 ),
134 'lazy' => array(
135 'add_missing_dimensions' => false,
136 'lazy_iframes' => false,
137 'lazy_images' => false,
138 'lazy_videos' => false,
139 'video_facade' => false,
140 ),
141 'minify' => array(
142 'async_css' => false,
143 'combine_css' => false,
144 'combine_js' => false,
145 'defer_js' => false,
146 'delay_js' => false,
147 'minify_css' => false,
148 'minify_html' => false,
149 'minify_js' => false,
150 'remove_query_strings' => false,
151 ),
152 'preloader' => array(
153 'enabled' => false,
154 'warm_on_comment' => false,
155 'warm_on_publish' => false,
156 ),
157 'resource-hints' => array(
158 'enabled' => false,
159 'lcp_preload' => false,
160 'preconnect' => false,
161 ),
162 'score' => array(
163 'enabled' => false,
164 ),
165 );
166
167 /**
168 * On disk at all, active or not — what an OFFER should gate on. A site that
169 * already has xSpeed has decided about it; re-offering one the user
170 * deactivated is nagging.
171 */
172 public static function is_installed(): bool
173 {
174 return isset(self::installed_plugins()[self::PLUGIN_FILE]);
175 }
176
177 public static function is_active(): bool
178 {
179 if (! function_exists('is_plugin_active')) {
180 self::load_plugin_api();
181 }
182
183 return function_exists('is_plugin_active') && is_plugin_active(self::PLUGIN_FILE);
184 }
185
186 /**
187 * Has xSpeed ever run on this site?
188 *
189 * The same signal xSpeed's own Settings::set_defaults() gates on: the option
190 * is created on first activation and survives deactivation, so its presence
191 * means these settings are the user's. Its absence means a genuinely fresh
192 * install, whose recommended profile has not been seeded yet.
193 *
194 * This is the question `prepare()` needs, and unlike "are the files on disk"
195 * it is immune to install ordering — activation can only run on an unpacked
196 * plugin, so by the time a host calls prepare() the files are always there.
197 */
198 public static function has_settings(): bool
199 {
200 return false !== get_option(self::SETTINGS_OPTION, false);
201 }
202
203 /**
204 * Did THIS class put xSpeed's settings here?
205 *
206 * The snapshot is prepare()'s receipt: written when it succeeds, cleared by
207 * finish(). Its presence is the only evidence that the page cache is ours to
208 * switch on — without it, prepare() refused because the site already had
209 * settings, and touching them would overwrite the choice the guard protects.
210 */
211 public static function is_ours(): bool
212 {
213 return false !== get_option(self::SNAPSHOT_OPTION, false);
214 }
215
216 public static function is_supported(): bool
217 {
218 global $wp_version;
219
220 return version_compare((string) $wp_version, self::REQUIRES_WP, '>=')
221 && version_compare(PHP_VERSION, self::REQUIRES_PHP, '>=');
222 }
223
224 /**
225 * Write the state xSpeed should come up with. Call BEFORE activation.
226 *
227 * Not a style choice. xSpeed's activation reads what is already stored rather
228 * than stamping over it: module seeders skip rows that exist, and
229 * Cache::restore_dropin_if_enabled() sees the page-cache flag already true and
230 * installs advanced-cache.php and WP_CACHE itself. Configuring AFTER activation
231 * instead does not work and does not complain — Settings_Manager::update()
232 * resolves modules through Module_Registry, which is empty for a plugin
233 * activated part-way through the request, so it returns without writing.
234 *
235 * Refuses when xSpeed has settings here already — see has_settings(). Not when
236 * its files are on disk: activation can only run on an unpacked plugin, so the
237 * files are always present by the time a host calls this, and guarding on them
238 * meant nothing was ever written.
239 *
240 * Even so, only the keys in INITIAL_SETTINGS are written; anything else in a row
241 * is preserved. Option rows survive plugin deletion, so a site with no xSpeed
242 * can still carry lazy-load exclusions, a preloader sitemap or browser-cache
243 * lifetimes from a previous install, and there is no reason to destroy them to
244 * turn four switches off.
245 *
246 * $enable_cache false installs and configures xSpeed WITHOUT taking the
247 * page cache — for a site where the Detector found an incumbent. Do not
248 * mistake it for "skip this call": skipping means xSpeed's own
249 * set_defaults() sees a fresh install and seeds its recommended profile,
250 * switching on gzip, browser caching and minification. On the very site
251 * where you promised to touch nothing, the user would get markup
252 * rewriting they never agreed to, and no page cache either. Writing the
253 * settings option is what suppresses that first-run path, so this still
254 * has to run — just with the page cache left off.
255 *
256 * @param bool $enable_cache Whether xSpeed should come up owning the page
257 * cache. Pass Detector::is_field_clear().
258 *
259 * @return bool False means xSpeed already has settings here, so they are the
260 * user's and not ours to replace. Not something to retry.
261 */
262 public static function prepare(bool $enable_cache = true): bool
263 {
264 if (self::has_settings()) {
265 /*
266 * A leftover snapshot says these settings are an earlier
267 * prepare() of ours that never reached finish() or rollback() —
268 * the request died between them, which on shared hosting is an
269 * activation that timed out. Left alone it is worse than
270 * untidy: a stranded `cache_enabled => true` makes xSpeed's own
271 * restore_dropin_if_enabled() install the drop-in the next time
272 * the user activates by hand, which is page caching nobody asked
273 * for. Undo it and start again.
274 */
275 if (! self::is_ours()) {
276 return false;
277 }
278
279 self::rollback();
280
281 if (self::has_settings()) {
282 return false;
283 }
284 }
285
286 $snapshot = array();
287 $settings = get_option(self::SETTINGS_OPTION, null);
288
289 $snapshot[self::SETTINGS_OPTION] = $settings;
290 update_option(
291 self::SETTINGS_OPTION,
292 array_merge(
293 is_array($settings) ? $settings : array(),
294 array(self::PAGE_CACHE_KEY => $enable_cache)
295 ),
296 false
297 );
298
299 foreach (self::INITIAL_SETTINGS as $slug => $values) {
300 $option = self::MODULE_PREFIX . $slug;
301 $stored = get_option($option, null);
302
303 $snapshot[$option] = $stored;
304 update_option(
305 $option,
306 array_merge(is_array($stored) ? $stored : array(), $values),
307 false
308 );
309 }
310
311 update_option(self::SNAPSHOT_OPTION, $snapshot, false);
312
313 return true;
314 }
315
316 /**
317 * Undo prepare() when the install did not survive to activation.
318 *
319 * Restores what was there rather than deleting: a row prepare() merged into
320 * belonged to the site before we touched it. A stranded `cache_enabled => true`
321 * would otherwise tell a LATER hand-install to bring up page caching nobody
322 * asked for.
323 */
324 public static function rollback(): void
325 {
326 $snapshot = get_option(self::SNAPSHOT_OPTION, null);
327
328 if (! is_array($snapshot)) {
329 return;
330 }
331
332 foreach ($snapshot as $option => $previous) {
333 if (null === $previous) {
334 delete_option($option);
335 continue;
336 }
337
338 update_option($option, $previous, false);
339 }
340
341 delete_option(self::SNAPSHOT_OPTION);
342 }
343
344 /**
345 * Finish up. Call AFTER a successful activation.
346 *
347 * Activation arms the setup-wizard redirect unconditionally, so nothing
348 * prepare() does can prevent it — it has to be cleared here. Clearing the
349 * redirect deliberately does not mark onboarding complete; the user did not
350 * complete it, and the wizard stays in xSpeed's own menu.
351 *
352 * The page-cache fallback is for when the drop-in or wp-config.php write was
353 * refused: better the long way round than a cache plugin that caches nothing.
354 *
355 * @return bool True when xSpeed is active and page caching is live.
356 */
357 public static function finish(): bool
358 {
359 if (! self::is_active()) {
360 return false;
361 }
362
363 // Without prepare()'s receipt these settings are not ours, and enabling
364 // here would overwrite the very choice the guard protects — two lines
365 // later in the same documented flow.
366 $ours = self::is_ours();
367
368 // Cancelling the wizard redirect is safe either way: the host caused
369 // the activation, so nobody asked to be sent to onboarding.
370 delete_option(self::WIZARD_REDIRECT_OPTION);
371
372 if (! $ours) {
373 return self::page_cache_is_live();
374 }
375
376 /*
377 * prepare( false ) asked for an install that does NOT take the page
378 * cache, and that choice is recorded in the settings row. Enabling
379 * here would install advanced-cache.php over the incumbent's — the
380 * exact conflict the Detector exists to prevent — so the flag is
381 * read back rather than assumed.
382 *
383 * The snapshot still goes: activation succeeded, so there is nothing
384 * to roll back to, and a stranded receipt makes the next prepare()
385 * undo settings that are now legitimately the site's.
386 */
387 if (! self::page_cache_requested()) {
388 delete_option(self::SNAPSHOT_OPTION);
389
390 return false;
391 }
392
393 if (! self::page_cache_is_live()) {
394 self::enable_page_cache();
395 }
396
397 // Activation succeeded, so there is nothing left to roll back to.
398 delete_option(self::SNAPSHOT_OPTION);
399
400 return self::page_cache_is_live();
401 }
402
403 /**
404 * Whether page caching took effect, not merely that it was requested: the
405 * flag proves nothing without the drop-in and the WP_CACHE constant.
406 *
407 * The constant is read from wp-config.php, not from `defined()`. Enabling the
408 * cache WRITES it there, and nothing defines it for the request already in
409 * flight — so on a clean site, exactly the site the detector green-lights, the
410 * runtime check is false the whole way through a successful install. Trusting
411 * it made finish() re-run the entire enable and then report failure on a
412 * perfectly good install.
413 */
414 /**
415 * Whether the stored settings ASK for page caching — what prepare() was
416 * told, not whether it took effect.
417 *
418 * The distinction matters to finish(): "not live yet" is a job to do,
419 * "not requested" is a decision to respect, and page_cache_is_live()
420 * cannot tell them apart.
421 */
422 public static function page_cache_requested(): bool
423 {
424 $options = get_option(self::SETTINGS_OPTION, array());
425
426 return is_array($options) && ! empty($options[self::PAGE_CACHE_KEY]);
427 }
428
429 public static function page_cache_is_live(): bool
430 {
431 $options = get_option(self::SETTINGS_OPTION, array());
432
433 return is_array($options)
434 && ! empty($options[self::PAGE_CACHE_KEY])
435 && file_exists(WP_CONTENT_DIR . '/advanced-cache.php')
436 && self::wp_cache_constant_written();
437 }
438
439 /**
440 * Is WP_CACHE set to true in wp-config.php?
441 *
442 * Delegated to Detector, which already reads that file and understands the
443 * spellings hosts use (`true`, `1`, `'1'`, `TRUE`) and which of them are
444 * literals. A second parser here disagreed with it on both the accepted
445 * values and where wp-config.php lives; two copies of one lookup drift.
446 *
447 * Detector memoises, and the constant may have been written moments ago by
448 * the very call we are checking, so drop the memo first.
449 */
450 private static function wp_cache_constant_written(): bool
451 {
452 if (defined('WP_CACHE') && WP_CACHE) {
453 return true;
454 }
455
456 if (! class_exists(__NAMESPACE__ . '\\Detector')) {
457 // Setup is meant to be copied alongside Detector. Without it the
458 // file cannot be read, and "we could not tell" must not read as
459 // "it is live".
460 return false;
461 }
462
463 Detector::invalidate();
464 $report = Detector::inspect();
465
466 return isset($report['wp_cache']['state']) && 'true' === $report['wp_cache']['state'];
467 }
468
469 /**
470 * Fallback: switch page caching on the long way.
471 *
472 * The flag cannot simply be written — Settings_Manager rejects that key by
473 * name, because it is what drives the drop-in install and the wp-config.php
474 * edit, so a bare write leaves a site claiming a cache it does not have (which
475 * Detector then reads as unknown-occupied). Mirrors Rest_Api::toggle_cache().
476 *
477 * That REST route would be tidier, but a host that just activated xSpeed
478 * part-way through a request cannot reach it: rest_api_init has already fired.
479 */
480 private static function enable_page_cache(): void
481 {
482 if (! class_exists('\\XSpeed\\Cache') || ! class_exists('\\XSpeed\\Settings')) {
483 return;
484 }
485
486 try {
487 $state = \XSpeed\Cache::toggle(true);
488 \XSpeed\Settings::update(
489 array(self::PAGE_CACHE_KEY => ! empty($state['enabled']))
490 );
491 } catch (\Throwable $e) {
492 // A cache that would not switch on must never take the host's
493 // own operation down with it.
494 return;
495 }
496
497 // Site state just changed under Detector's feet; drop its memo so a
498 // later question in this request does not get the pre-install answer.
499 if (class_exists(__NAMESPACE__ . '\\Detector')) {
500 Detector::invalidate();
501 }
502 }
503
504 /**
505 * @return array<string,array>
506 */
507 private static function installed_plugins(): array
508 {
509 if (! function_exists('get_plugins')) {
510 self::load_plugin_api();
511 }
512
513 return function_exists('get_plugins') ? get_plugins() : array();
514 }
515
516 private static function load_plugin_api(): void
517 {
518 if (defined('ABSPATH') && file_exists(ABSPATH . 'wp-admin/includes/plugin.php')) {
519 require_once ABSPATH . 'wp-admin/includes/plugin.php';
520 }
521 }
522 }
523 }
524