PluginProbe
Templately – Elementor & Gutenberg Template Library: 6500+ Free & Pro Ready Templates And Cloud! / 3.8.0
Templately – Elementor & Gutenberg Template Library: 6500+ Free & Pro Ready Templates And Cloud! v3.8.0
3.8.0 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 All 112 releases
templately / includes / Utils / Caching.php

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

400 lines 14.3 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 /**
6 * The caching plugin Templately offers on the import dependency screen.
7 *
8 * Templately owns the OFFER — whether to put a row on the screen, and how often.
9 * It does not own what the plugin comes up as. Claiming the install is one option
10 * write immediately before activation:
11 *
12 * update_option( 'xspeed_installed_by', 'templately' );
13 *
14 * xSpeed reads that on its own activation and picks its own profile — every Free
15 * feature off, page caching on when nothing else owns it and refused when something
16 * does — and skips its own setup wizard so it does not interrupt ours. No settings
17 * writes from here, no enable call, no rollback path.
18 *
19 * A plain option rather than a constant or a filter because it is written at a moment
20 * when none of xSpeed's code has loaded and none can be relied on to exist.
21 *
22 * See docs/guides/installing-from-another-plugin.md in the xSpeed repo. This class
23 * replaced a copy-vendored detector that decided the cache question here and wrote
24 * xSpeed's settings by hand; that directory is deleted upstream, not versioned up.
25 */
26 class Caching {
27
28 /**
29 * The plugin we offer, and the Pro build that ships beside it.
30 *
31 * Named here rather than read from xSpeed because every question below has to be
32 * answerable on a site where xSpeed does not exist. `XSpeed\Host` is the supported
33 * API for everything else, but it only exists once xSpeed is active.
34 */
35 const PLUGIN_FILE = 'xspeed/xspeed.php';
36 const PRO_PLUGIN_FILE = 'xspeed-pro/xspeed-pro.php';
37 const PLUGIN_SLUG = 'xspeed';
38 const PLUGIN_NAME = 'xSpeed Cache';
39 const PLUGIN_ICON = 'https://ps.w.org/xspeed/assets/icon-256x256.png';
40 const PLUGIN_LINK = 'https://wordpress.org/plugins/xspeed/';
41
42 /**
43 * The one-shot trigger that tells xSpeed a host installed it.
44 *
45 * A trigger, not a record: activation spends it, moving the value where
46 * Host::installed_by() reads it afterwards.
47 */
48 const INSTALLED_BY_OPTION = 'xspeed_installed_by';
49
50 /** Our slug, as xSpeed records it. */
51 const INSTALLER_SLUG = 'templately';
52
53 /**
54 * Where xSpeed keeps its settings. Read only to answer "has this site had xSpeed
55 * before" — an option row outlives plugin deletion, and it is still the user's
56 * answer about a plugin we would otherwise re-offer.
57 */
58 const SETTINGS_OPTION = 'xspeed_options';
59
60 /**
61 * What xSpeed needs to run. Templately's own floor is lower, and an install that
62 * cannot activate is worse than an offer never made.
63 */
64 const REQUIRES_WP = '6.0';
65 const REQUIRES_PHP = '7.4';
66
67 /**
68 * When the suggestion was first put in front of this site, as a Unix timestamp.
69 *
70 * Site-scoped rather than per-user: whether this site wants a page cache is a fact
71 * about the site, and a second administrator should not be re-asked a question the
72 * first one already answered. Delete this option to offer it again — that is the
73 * supported reset, for support staff and for testing.
74 */
75 const OFFER_SHOWN_OPTION = 'templately_caching_offer_shown';
76
77 /**
78 * How long after the first showing the row keeps appearing.
79 *
80 * The dependency step re-fetches whenever the wizard is reopened or the user steps
81 * back and forward, so a flag set on first render would make the row vanish
82 * underneath someone still deciding about it.
83 */
84 const OFFER_GRACE = 1800;
85
86 /**
87 * How long before a declined offer may be made again.
88 *
89 * The ONLY suppression that expires, and it only ever applies to a decline. Every
90 * other reason to withhold the row is permanent by construction and outlives this
91 * window: the plugin is on disk (free or Pro), its settings are here, or the site
92 * cannot run it. Accepting installs the plugin, so an accepted offer is never made
93 * twice either.
94 */
95 const OFFER_COOLDOWN = MONTH_IN_SECONDS;
96
97 /**
98 * The cross-plugin record of what this site already decided about xSpeed.
99 *
100 * Shared, not ours: EmbedPress, Essential Addons and Templately all offer the same
101 * plugin, and each one keeping its own answer means a user who says no three times
102 * has said no once as far as any of them can tell. Namespaced `wpdeveloper_` after
103 * `wpdeveloper_plugins_data`, the convention the shared notice library already
104 * uses, and deliberately OUTSIDE the `xspeed_` namespace — xSpeed's uninstall.php
105 * deletes every option it owns, so a decision stored there would be erased by the
106 * very act it is meant to remember.
107 *
108 * Shape, all keys optional to a reader:
109 *
110 * [
111 * 'offered_by' => 'templately', // slug of whoever last put the offer up
112 * 'offered_at' => 1757462400, // when it went up
113 * 'outcome' => 'offered', // see below
114 * 'outcome_at' => 1757462400,
115 * ]
116 *
117 * `outcome` is one of:
118 *
119 * - `offered` — the row went out; nobody has answered yet.
120 * - `accepted` — a host installed it. Written immediately before activation.
121 * - `declined` — the user said no in a way that was meant to stick (a dismissed
122 * promo, a "never show again"). Templately never writes it: its own
123 * decline is the local timer above, which expires. A host with a
124 * permanent opt-out control should write it there.
125 * - `removed` — accepted, then taken off the site. NOT stored: it is derived, so
126 * a deletion performed outside any of our code is still seen. The
127 * name exists so a reader can talk about the state.
128 *
129 * Terminal outcomes are terminal. Nothing here re-offers past one.
130 *
131 * @see docs/guides/installing-from-another-plugin.md in the xSpeed repo.
132 */
133 const OFFER_RECORD_OPTION = 'wpdeveloper_xspeed_offer';
134
135 /**
136 * On disk at all, active or not — free or Pro.
137 *
138 * Presence, not activation. A site that has it has decided about it, including a
139 * user who installed it and switched it off, and re-offering that is nagging.
140 * Offering Free to a site running Pro would be worse still: a downgrade.
141 */
142 public static function is_installed(): bool {
143 $plugins = Helper::get_plugins();
144
145 return isset( $plugins[ self::PLUGIN_FILE ] ) || isset( $plugins[ self::PRO_PLUGIN_FILE ] );
146 }
147
148 /**
149 * Has xSpeed ever run here? Its settings row survives deactivation and deletion.
150 */
151 public static function has_settings(): bool {
152 return false !== get_option( self::SETTINGS_OPTION, false );
153 }
154
155 public static function is_supported(): bool {
156 global $wp_version;
157
158 return version_compare( (string) $wp_version, self::REQUIRES_WP, '>=' )
159 && version_compare( PHP_VERSION, self::REQUIRES_PHP, '>=' );
160 }
161
162 /**
163 * Whether the suggestion has already had its turn.
164 *
165 * A window rather than a permanent flag. Inside OFFER_GRACE the row keeps showing.
166 * Past OFFER_COOLDOWN the answer has aged out and may be asked again. Between the
167 * two, it is spent.
168 */
169 public static function has_been_offered(): bool {
170 $shown = (int) get_option( self::OFFER_SHOWN_OPTION, 0 );
171
172 if ( $shown <= 0 ) {
173 return false;
174 }
175
176 $age = time() - $shown;
177
178 return $age > self::OFFER_GRACE && $age < self::OFFER_COOLDOWN;
179 }
180
181 /**
182 * Record that the row went out.
183 *
184 * Re-arms only once the previous showing has aged out. Rewriting on every re-render
185 * would mean the offer never expires, and the wizard re-fetches this step often.
186 */
187 public static function mark_offered() {
188 $shown = (int) get_option( self::OFFER_SHOWN_OPTION, 0 );
189
190 if ( $shown <= 0 || ( time() - $shown ) >= self::OFFER_COOLDOWN ) {
191 update_option( self::OFFER_SHOWN_OPTION, time(), false );
192 self::record_offer();
193
194 return;
195 }
196
197 // The row is up but the clock is already running, so the shared record has
198 // nothing new to learn — except on a site that was mid-window when this
199 // release landed, where it does not exist yet. Writing it on every fetch
200 // would stamp `offered_at` with the current second forever, and a sibling
201 // pacing itself off that field would never see the offer age out.
202 if ( '' === self::outcome() ) {
203 self::record_offer();
204 }
205 }
206
207 /**
208 * The shared record, always an array so callers can read it without guarding.
209 */
210 public static function offer_record(): array {
211 $record = get_option( self::OFFER_RECORD_OPTION, array() );
212
213 return is_array( $record ) ? $record : array();
214 }
215
216 /**
217 * Note in the shared record that the row went out.
218 *
219 * Never downgrades an answer. A site that already accepted or declined has told us
220 * something; putting it back to `offered` because the row rendered again would lose
221 * that, and the row should not have rendered in the first place.
222 */
223 public static function record_offer() {
224 $record = self::offer_record();
225
226 if ( in_array( self::outcome(), array( 'accepted', 'declined' ), true ) ) {
227 return;
228 }
229
230 $record['offered_by'] = self::INSTALLER_SLUG;
231 $record['offered_at'] = time();
232 $record['outcome'] = 'offered';
233 $record['outcome_at'] = time();
234
235 update_option( self::OFFER_RECORD_OPTION, $record, false );
236 }
237
238 /**
239 * Write a terminal answer into the shared record.
240 *
241 * @param string $outcome `accepted` or `declined`.
242 */
243 public static function record_outcome( string $outcome ) {
244 if ( ! in_array( $outcome, array( 'accepted', 'declined' ), true ) ) {
245 return;
246 }
247
248 $record = self::offer_record();
249
250 if ( empty( $record['offered_by'] ) ) {
251 $record['offered_by'] = self::INSTALLER_SLUG;
252 $record['offered_at'] = time();
253 }
254
255 $record['outcome'] = $outcome;
256 $record['outcome_at'] = time();
257
258 update_option( self::OFFER_RECORD_OPTION, $record, false );
259 }
260
261 /**
262 * The recorded outcome, or '' when nobody has written a usable one.
263 *
264 * Scalar-guarded because three plugins write this row and only one of them is
265 * this file. A nested array would otherwise be cast to the string 'Array' — a
266 * PHP notice, which the test rig turns into an exception and WP_DEBUG_DISPLAY
267 * prints into the REST response.
268 */
269 public static function outcome(): string {
270 $record = self::offer_record();
271
272 if ( ! isset( $record['outcome'] ) || ! is_scalar( $record['outcome'] ) ) {
273 return '';
274 }
275
276 return (string) $record['outcome'];
277 }
278
279 /**
280 * Has this site already answered the question, whoever asked it?
281 *
282 * The check that makes the offer survive a deletion. `accepted` plus an absent
283 * plugin is a user who installed it and then took it off — the clearest "no" a
284 * user can give, and the one every other guard here misses, because deleting a
285 * plugin runs its uninstaller: `xspeed_options` goes with it, so has_settings()
286 * forgets, and OFFER_COOLDOWN then re-offers a month later. Derived rather than
287 * stored so a deletion done from the Plugins screen — with none of our code
288 * running — still counts.
289 */
290 public static function was_answered(): bool {
291 $outcome = self::outcome();
292
293 if ( 'declined' === $outcome ) {
294 return true;
295 }
296
297 if ( 'accepted' === $outcome ) {
298 return ! self::is_installed();
299 }
300
301 return false;
302 }
303
304 /**
305 * The dependency row, in the same shape as every other entry on that screen.
306 *
307 * The name, icon and link are the real plugin's: the user is agreeing to install a
308 * specific thing and should be able to see and check what it is. `installed` is
309 * always false — the offer is withheld outright when the plugin is present — and
310 * false is what keeps the checkbox enabled. `mustHave` is omitted: a suggestion,
311 * not a requirement.
312 */
313 public static function dependency_entry(): array {
314 return array(
315 'name' => self::PLUGIN_NAME,
316 'icon' => self::PLUGIN_ICON,
317 'plugin_file' => self::PLUGIN_FILE,
318 'plugin_original_slug' => self::PLUGIN_SLUG,
319 'is_pro' => false,
320 'installed' => false,
321 'link' => self::PLUGIN_LINK,
322 );
323 }
324
325 /**
326 * Whether to offer the caching plugin alongside whatever the pack itself asked for.
327 *
328 * Deliberately not conditional on anything already owning the page cache. xSpeed
329 * installs beside another cache plugin and stands down from the cache itself — that
330 * is its decision, made at its own activation, and asking it here would only be
331 * asking on an earlier request than the one that matters.
332 *
333 * Cheapest checks first, and the order is load-bearing:
334 *
335 * - Can this user even accept? The dependency screen is readable at `delete_posts`,
336 * so a contributor can open the wizard; installing needs `install_plugins`.
337 * Without this they would spend the site's one offer on themselves.
338 * - Already answered, by us or by any sibling plugin. Permanent, and checked ahead
339 * of our own timer because it outranks it: OFFER_GRACE keeps the row up for half
340 * an hour, which was long enough to install xSpeed, delete it, and be offered it
341 * again in the same sitting.
342 * - Already offered, within the window.
343 * - Already present — free or Pro — or already carrying xSpeed's settings.
344 * - The site can run it.
345 */
346 public static function should_offer(): bool {
347 if ( ! Helper::current_user_can( 'install_plugins' ) ) {
348 return false;
349 }
350
351 if ( self::was_answered() ) {
352 return false;
353 }
354
355 if ( self::has_been_offered() ) {
356 return false;
357 }
358
359 return ! self::is_installed()
360 && ! self::has_settings()
361 && self::is_supported();
362 }
363
364 /**
365 * Claim the install, so xSpeed comes up as a host install rather than a hand one.
366 *
367 * Call immediately before activating, and never speculatively. It is a one-shot
368 * trigger that changes what activation does, not a record of intent — an install
369 * that dies between this and the activation arms the NEXT activation on the site,
370 * whoever starts it.
371 *
372 * Also settles the shared record at `accepted`. Written here rather than after a
373 * successful activation on purpose: an install that got this far has been agreed
374 * to, and an activation that then fails still leaves files on disk. Recording the
375 * answer is what stops the site being asked again once those files are removed.
376 */
377 public static function claim_install() {
378 update_option( self::INSTALLED_BY_OPTION, self::INSTALLER_SLUG, false );
379
380 self::record_outcome( 'accepted' );
381 }
382
383 /**
384 * What the install came up as, for reporting. Null when xSpeed is not active or is
385 * older than the release that introduced the API.
386 *
387 * `conflict-safe` is a success, not a failure: it means another plugin was already
388 * caching and xSpeed stood down, which is the designed outcome.
389 *
390 * @return array|null
391 */
392 public static function install_status() {
393 if ( ! class_exists( '\XSpeed\Host' ) ) {
394 return null;
395 }
396
397 return \XSpeed\Host::status();
398 }
399 }
400