PluginProbe
Templately – Elementor & Gutenberg Template Library: 6500+ Free & Pro Ready Templates And Cloud! / 3.7.5
Templately – Elementor & Gutenberg Template Library: 6500+ Free & Pro Ready Templates And Cloud! v3.7.5
3.7.5 3.7.4 3.7.3 3.7.2 1-final 3.7.1 3.7.0 3.6.8 3.6.7 3.6.6 3.6.5 3.6.4 3.6.3 3.6.2 3.6.1 3.0.3 3.0.4 3.0.5 3.0.6 3.0.7 3.0.8 3.0.9 3.1.0 3.1.1 3.1.10 All 111 releases
templately / includes / Utils / Caching.php

Caching.php in Templately – Elementor & Gutenberg Template Library: 6500+ Free & Pro Ready Templates And Cloud! 3.7.5, at includes/Utils/Caching.php

402 lines 16.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace Templately\Utils;
4
5 use WPDeveloper\PageCacheSafety\Detector;
6
7 /**
8 * The caching solution Templately offers on the import dependency screen.
9 *
10 * Everything vendor-specific about the recommended plugin — its file, slug, display details,
11 * option names and the settings it should come up with — is confined to the constants at the
12 * top of this class. Nothing else in the plugin names it. Swapping the recommendation for a
13 * different caching plugin is an edit to this block and its INITIAL_SETTINGS map; no caller
14 * changes.
15 *
16 * Two jobs: decide whether to offer a caching solution at all, and — only once the user has
17 * left the row ticked and it has actually installed — leave it configured the way the offer
18 * implied.
19 */
20 class Caching {
21
22 /**
23 * The recommended plugin. The display fields are what the user sees on the dependency
24 * row and are deliberately the plugin's real name and listing, so the row says what it
25 * installs.
26 */
27 const PLUGIN_FILE = 'xspeed/xspeed.php';
28 const PLUGIN_SLUG = 'xspeed';
29 const PLUGIN_NAME = 'xSpeed Cache';
30 const PLUGIN_ICON = 'https://ps.w.org/xspeed/assets/icon-256x256.png';
31 const PLUGIN_LINK = 'https://wordpress.org/plugins/xspeed/';
32
33 /**
34 * Where the recommended plugin keeps its settings.
35 */
36 const SETTINGS_OPTION = 'xspeed_options';
37 const MODULE_PREFIX = 'xspeed_module_';
38
39 /**
40 * The option its activation sets to force a first-run setup wizard redirect. Written
41 * unconditionally on activation and consumed on the next admin_init.
42 */
43 const WIZARD_REDIRECT_OPTION = 'xspeed_redirect_to_onboarding';
44
45 /**
46 * When the suggestion was first put in front of this site, as a Unix timestamp.
47 *
48 * The offer is made once. A user who ticked it has the plugin; a user who unticked it
49 * said no, and asking again on their next import is nagging. Delete this option to offer
50 * it again — that is the supported reset, for support staff and for testing.
51 *
52 * Site-scoped rather than per-user: whether this site wants a page cache is a fact about
53 * the site, and a second administrator should not be re-asked a question the first one
54 * already answered.
55 */
56 const OFFER_SHOWN_OPTION = 'templately_caching_offer_shown';
57
58 /**
59 * How long after the first showing the row keeps appearing.
60 *
61 * Without this, "once" would mean once per HTTP request rather than once per user. The
62 * dependency step re-fetches whenever the wizard is reopened or the user steps back and
63 * forward, so a flag set on first render would make the row vanish underneath someone
64 * who was still deciding about it. Inside the window the answer is unchanged; after it,
65 * the offer is spent.
66 */
67 const OFFER_GRACE = 1800;
68
69 /**
70 * Version floors, pinned rather than read from plugins_api().
71 *
72 * The recommended plugin asks for more than Templately advertises (readme.txt:
73 * WordPress 5.0, PHP 7.2), and Installer::check_compatibility() turns a mismatch into a
74 * hard failure of the import's plugin step — so a pre-ticked row on an older site is a
75 * broken import, not a declined suggestion. Fetching the real numbers per request would
76 * mean a network call on a screen the user is already waiting on. The cost of pinning
77 * them is that a floor change needs a matching edit here.
78 */
79 const REQUIRES_WP = '6.0';
80 const REQUIRES_PHP = '7.4';
81
82 /**
83 * The settings a Templately-installed caching plugin should come up with.
84 *
85 * The whole desired end state, not a list of things to switch off, because pre-writing
86 * SETTINGS_OPTION suppresses whatever first-run path the plugin otherwise uses to seed
87 * these. Browser caching defaults to `false` in its own schema and was only ever on
88 * because of that path — so leaving it out here silently turned it off. Anything wanted
89 * on has to be said out loud.
90 *
91 * Page caching is the only thing Templately switches on. The row the user ticked offered
92 * a page cache, so that is what they get — browser caching, minification, lazy loading,
93 * resource hints and the rest all stay off, whatever the plugin would have enabled for
94 * itself.
95 *
96 * Every module is written explicitly, including the ones that would be off anyway.
97 * Measured against 1.2.0 with no rows written at all: lazy, resource-hints, fonts and
98 * preloader come up ON — their schema defaults are true — so writing those is what turns
99 * them off. gzip, minify and browser-cache come up off, but only because pre-writing
100 * SETTINGS_OPTION happens to suppress the plugin's first-run seeding; a normal install
101 * brings all three up ON. Leaning on that side effect is what silently re-enabled browser
102 * caching once already, so the redundant rows stay.
103 *
104 * Deliberately absent, and left exactly as the plugin sets them: its GDPR consent
105 * requirement, its Cloudflare purge behaviour, and its object-cache flag. None is an
106 * optimisation, and for the first of them off would be the wrong answer. A blanket
107 * "everything except browser caching" rule would have caught all three, and would
108 * silently swallow whatever module the plugin ships next. The cost of naming them is
109 * that a future opt-in-by-default optimisation has to be added here by hand.
110 *
111 * Keys are module slugs, values the fields written to MODULE_PREFIX . <slug>. Verified
112 * against the recommended plugin at 1.2.0.
113 */
114 const INITIAL_SETTINGS = array(
115 'browser-cache' => array(
116 'enabled' => false,
117 ),
118 'gzip' => array(
119 'gzip_enabled' => false,
120 ),
121 'minify' => array(
122 'minify_html' => false,
123 'minify_css' => false,
124 ),
125 'lazy' => array(
126 'lazy_images' => false,
127 'lazy_iframes' => false,
128 'lazy_videos' => false,
129 'add_missing_dimensions' => false,
130 ),
131 'resource-hints' => array(
132 'enabled' => false,
133 'lcp_preload' => false,
134 'preconnect' => false,
135 ),
136 'fonts' => array(
137 'font_display_swap' => false,
138 ),
139 'preloader' => array(
140 'warm_on_publish' => false,
141 ),
142 );
143
144 /**
145 * Present on disk at all, active or not.
146 */
147 public static function is_installed(): bool {
148 return isset( Helper::get_plugins()[ self::PLUGIN_FILE ] );
149 }
150
151 public static function is_active(): bool {
152 return Helper::is_plugin_active( self::PLUGIN_FILE );
153 }
154
155 /**
156 * Whether the suggestion has already had its turn, grace window elapsed.
157 */
158 public static function has_been_offered(): bool {
159 $shown = (int) get_option( self::OFFER_SHOWN_OPTION, 0 );
160
161 return $shown > 0 && ( time() - $shown ) > self::OFFER_GRACE;
162 }
163
164 /**
165 * Record that the row went out. First writing wins, so the grace window is measured from
166 * the first showing rather than being pushed forward by every re-render.
167 */
168 public static function mark_offered() {
169 if ( ! get_option( self::OFFER_SHOWN_OPTION ) ) {
170 update_option( self::OFFER_SHOWN_OPTION, time(), false );
171 }
172 }
173
174 /**
175 * The dependency row, in the same shape as every other entry on that screen.
176 *
177 * `installed` is always false and not worth deriving: the offer is withheld outright
178 * when the plugin is on disk at all, so anything reaching here is a fresh install. false
179 * is also what keeps the checkbox enabled, which is the point of offering it. `mustHave`
180 * is omitted — this is a suggestion, not a requirement.
181 */
182 public static function dependency_entry(): array {
183 return array(
184 'name' => self::PLUGIN_NAME,
185 'icon' => self::PLUGIN_ICON,
186 'plugin_file' => self::PLUGIN_FILE,
187 'plugin_original_slug' => self::PLUGIN_SLUG,
188 'is_pro' => false,
189 'installed' => false,
190 'link' => self::PLUGIN_LINK,
191 );
192 }
193
194 /**
195 * Whether to offer a caching solution alongside whatever the pack itself asked for.
196 *
197 * Three conditions, cheapest first. The first two are Templately's own, and the shared
198 * detector cannot answer either:
199 *
200 * - Already offered once. See OFFER_SHOWN_OPTION.
201 * - Already on disk. We only ever offer to put it there. A site that has it — running or
202 * not — has made its own decision about that plugin, and a row for something already
203 * sitting in wp-content is noise.
204 * - Version floors, per the constants above.
205 *
206 * Only then the detector, which is the sole authority on whether anything owns the page
207 * cache. The order is load-bearing, not incidental: the detector deliberately does not
208 * catalogue the plugin we recommend — it answers "is anything *else* here", from a site
209 * where that plugin may not be installed at all. An active copy with page caching
210 * switched off leaves no drop-in, matches no catalogue entry, and classifies as
211 * `unclaimed`. Consult the detector first and you offer users a plugin they already run.
212 *
213 * Read-only throughout: deciding installs, activates, and configures nothing.
214 */
215 public static function should_offer(): bool {
216 if ( self::has_been_offered() ) {
217 return false;
218 }
219
220 if ( self::is_installed() ) {
221 return false;
222 }
223
224 global $wp_version;
225
226 if ( version_compare( $wp_version, self::REQUIRES_WP, '<' )
227 || version_compare( PHP_VERSION, self::REQUIRES_PHP, '<' ) ) {
228 return false;
229 }
230
231 self::load_detector();
232
233 return Detector::is_field_clear();
234 }
235
236 /**
237 * Load the vendored page-cache detector.
238 *
239 * Copy-vendored from xSpeed Free: WPDevelopers/xspeed, branch
240 * feat/portable-page-cache-detector (PR #297), page-cache-safety/, at commit
241 * 2565b33fc90be8c3dae90aa1f0f1dd1f53339f14.
242 *
243 * That repo is the source of truth. Fixes go THERE and get re-copied here — never
244 * patched in place, or the parity test that keeps every copy honest stops meaning
245 * anything. Required at the point of use rather than at boot so it costs nothing on any
246 * other request; its own class_exists() wrapper makes it safe for another plugin on the
247 * same site to carry its own copy.
248 */
249 public static function load_detector() {
250 require_once TEMPLATELY_PATH . 'includes/Vendor/page-cache-safety/class-page-cache-safety.php';
251 }
252
253 /**
254 * Write the settings the plugin should come up with, BEFORE it is activated.
255 *
256 * This is the primary mechanism, and the ordering is the whole trick. Its activation
257 * reads what is already stored rather than stamping over it:
258 *
259 * - Each module seeds its option row only when one does not already exist, so a row we
260 * wrote first survives activation untouched.
261 * - Its cache drop-in restore runs during activation and, finding the page-cache flag
262 * already true, installs `advanced-cache.php` and writes the `WP_CACHE` constant
263 * itself — through the plugin's own supported path, on its own schedule.
264 *
265 * Verified at 1.2.0: with only this pre-write and no post-install step at all, the site
266 * comes up with page caching live, browser caching on, and every other front-end
267 * optimisation off.
268 *
269 * Doing it this way sidesteps the trap the post-install approach fell into. The plugin's
270 * settings manager resolves modules through a registry that is empty for a plugin
271 * activated part-way through the request — it returns without writing and without
272 * complaining. These are plain update_option() calls that need none of its code to be
273 * loaded, because at this point none of it is.
274 *
275 * Called immediately before activation so a failed download never leaves rows behind; if
276 * activation itself fails, forget_settings() takes them back out.
277 */
278 public static function prepare_settings() {
279 update_option( self::SETTINGS_OPTION, array( 'cache_enabled' => true ) );
280
281 foreach ( self::INITIAL_SETTINGS as $slug => $values ) {
282 update_option( self::MODULE_PREFIX . $slug, $values );
283 }
284 }
285
286 /**
287 * Undo prepare_settings() when the install did not survive to activation.
288 *
289 * Leaving these rows on a site that has no caching plugin is litter, and worse, a
290 * page-cache flag sitting there would tell a LATER hand-install to bring up caching the
291 * user never asked for.
292 */
293 public static function forget_settings() {
294 delete_option( self::SETTINGS_OPTION );
295
296 foreach ( array_keys( self::INITIAL_SETTINGS ) as $slug ) {
297 delete_option( self::MODULE_PREFIX . $slug );
298 }
299 }
300
301 /**
302 * Finish the job after a successful, Templately-driven activation.
303 *
304 * Only for an install Templately performed. Deliberately not hooked to `activated_plugin`:
305 * that fires when the user activates the plugin themselves from the Plugins screen, and
306 * reconfiguring their site off the back of an action they took elsewhere would be exactly
307 * the overreach this feature is trying not to commit.
308 *
309 * Two jobs left, because pre-writing cannot cover either:
310 *
311 * - The setup-wizard redirect, which activation arms unconditionally.
312 * - A safety net for page caching. The pre-write should already have caused the plugin to
313 * install its drop-in; if it did not, enable it the long way rather than leave the user
314 * with a cache plugin that caches nothing.
315 *
316 * Nothing here may break the import. A caching plugin that installed but did not
317 * configure is a worse outcome than one that did, and a far better one than a failed
318 * import.
319 */
320 public static function configure_after_install() {
321 if ( ! self::is_active() ) {
322 return;
323 }
324
325 self::suppress_setup_wizard();
326
327 if ( ! self::page_cache_is_live() ) {
328 self::enable_page_cache();
329 }
330 }
331
332 /**
333 * Drop the one-time wizard redirect the activation hook just armed.
334 *
335 * The one thing pre-writing cannot prevent: the flag is set unconditionally on
336 * activation, so it has to be cleared afterwards.
337 *
338 * The user came here to import a template. Bouncing them into another plugin's setup
339 * wizard on their next wp-admin page load is not what they asked for. The wizard stays in
340 * that plugin's own menu and can be run whenever they like — this cancels only the forced
341 * redirect, and deliberately does not mark onboarding "complete", which would be a claim
342 * about something the user never did.
343 */
344 private static function suppress_setup_wizard() {
345 delete_option( self::WIZARD_REDIRECT_OPTION );
346 }
347
348 /**
349 * Whether page caching actually took effect, rather than merely being requested.
350 *
351 * The stored flag on its own proves nothing — it is the drop-in and the constant that
352 * make WordPress serve from cache, and either can be missing if the filesystem or
353 * wp-config.php refused the write.
354 */
355 private static function page_cache_is_live(): bool {
356 $options = get_option( self::SETTINGS_OPTION, array() );
357
358 return ! empty( $options['cache_enabled'] )
359 && file_exists( WP_CONTENT_DIR . '/advanced-cache.php' )
360 && defined( 'WP_CACHE' ) && WP_CACHE;
361 }
362
363 /**
364 * Fallback: switch page caching on the long way.
365 *
366 * Only reached when the pre-write did not take — the drop-in restore declined, or the
367 * drop-in / wp-config.php write failed. The happy path never comes here.
368 *
369 * The page-cache flag cannot simply be written: the plugin's own settings manager rejects
370 * that key by name, because the flag is what drives the drop-in install and the
371 * wp-config.php edit. A bare write leaves a site claiming a cache it does not have —
372 * which the very detector that decided to offer it would then read as `unknown-occupied`.
373 * The toggle does the drop-in and the constant; the settings write after it persists the
374 * flag, mirroring the plugin's own REST handler.
375 *
376 * That REST route is its documented entry point and would be the tidier call, but it is
377 * unreachable here: the plugin was activated part-way through THIS request, so
378 * `rest_api_init` has already fired and its routes are not registered. These static calls
379 * are the same code that route runs.
380 */
381 private static function enable_page_cache() {
382 if ( ! class_exists( '\XSpeed\Cache' ) || ! class_exists( '\XSpeed\Settings' ) ) {
383 return;
384 }
385
386 try {
387 $state = \XSpeed\Cache::toggle( true );
388 \XSpeed\Settings::update( array( 'cache_enabled' => ! empty( $state['enabled'] ) ) );
389 } catch ( \Throwable $e ) {
390 // A failed cache switch-on must never take the import down with it.
391 Helper::log( 'Page cache could not be enabled: ' . $e->getMessage() );
392 return;
393 }
394
395 // The site's cache state just changed under the detector's feet. Anything asking
396 // again in this request must not get the pre-install answer back from its memo.
397 if ( class_exists( '\WPDeveloper\PageCacheSafety\Detector' ) ) {
398 Detector::invalidate();
399 }
400 }
401 }
402