PluginProbe
Bit Form – Contact Form, Payment Forms, Multi Step Forms, Calculator & Custom Form Builder / 3.3.1
Bit Form – Contact Form, Payment Forms, Multi Step Forms, Calculator & Custom Form Builder v3.3.1
3.3.1 V-3.3.0 3.2.2 3.2.1 3.2.0 3.1.4 3.1.3 3.1.2 3.1.1 3.1.0 V3.0.3 V3.0.2 -3.0.1 V_3.0.0 1.1.1 1.1.8 1.2 1.3 1.4 1.4.18 1.5.2 1.9 2.0 2.10.0 2.10.1 All 138 releases
bit-form / includes / Core / Util / CacheCompat.php

CacheCompat.php in Bit Form – Contact Form, Payment Forms, Multi Step Forms, Calculator & Custom Form Builder 3.3.1, at includes/Core/Util/CacheCompat.php

634 lines 18.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 /**
4 * Compatibility layer for caching / optimization plugins.
5 *
6 * The frontend needs the inline config (window.bf_globals) and the generated
7 * runtime bundle to execute in order. Optimizers that minify, combine, defer or
8 * delay JavaScript can reorder or withhold either one. Registers Bit Form's
9 * script patterns with each optimizer's exclusion filter, and stamps generic
10 * no-optimize attributes as a fallback for those that honor them.
11 *
12 * @since 3.2.2
13 */
14
15 namespace BitCode\BitForm\Core\Util;
16
17 use BitCode\BitForm\Core\Database\FormModel;
18
19 if (!defined('ABSPATH')) {
20 exit;
21 }
22
23 final class CacheCompat
24 {
25 /** True once a purge is queued for this request. */
26 private static $purgeScheduled = false;
27
28 /** Post IDs queued for purging. */
29 private static $purgePostIds = [];
30
31 /**
32 * Frontend script handle prefixes. bit-form-all-script covers both the free
33 * runtime handle (bit-form-all-script-test) and the pro data-view handle.
34 */
35 private const HANDLE_PREFIXES = [
36 'bit-form-all-script',
37 'bitform-bf-globals-',
38 'bitform-show-picker-bridge',
39 'bitforms_recaptcha',
40 ];
41
42 public static function register()
43 {
44 // WP Rocket
45 add_filter('rocket_delay_js_exclusions', [self::class, 'appendExclusionsArray']);
46 add_filter('rocket_exclude_defer_js', [self::class, 'appendExclusionsArray']);
47 add_filter('rocket_exclude_js', [self::class, 'appendExclusionsArray']);
48 add_filter('rocket_minify_excluded_external_js', [self::class, 'appendExclusionsArray']);
49 add_filter('rocket_excluded_inline_js_content', [self::class, 'appendExclusionsArray']);
50
51 // LiteSpeed Cache
52 add_filter('litespeed_optimize_js_excludes', [self::class, 'appendExclusionsArray']);
53 add_filter('litespeed_optm_js_defer_exc', [self::class, 'appendExclusionsArray']);
54
55 // Perfmatters
56 add_filter('perfmatters_delay_js_exclusions', [self::class, 'appendExclusionsArray']);
57 add_filter('perfmatters_defer_js_exclusions', [self::class, 'appendExclusionsArray']);
58
59 // Autoptimize (comma-separated string)
60 add_filter('autoptimize_filter_js_exclude', [self::class, 'appendExclusionsString']);
61
62 // SiteGround Optimizer (script handles)
63 add_filter('sgo_js_minify_exclude', [self::class, 'appendHandles']);
64 add_filter('sgo_javascript_combine_exclude', [self::class, 'appendHandles']);
65 add_filter('sgo_js_async_exclude', [self::class, 'appendHandles']);
66
67 // FlyingPress
68 add_filter('flying_press_exclude_from_delay:js', [self::class, 'appendExclusionsArray']);
69 add_filter('flying_press_exclude_from_defer:js', [self::class, 'appendExclusionsArray']);
70 add_filter('flying_press_exclude_from_minify:js', [self::class, 'appendExclusionsArray']);
71
72 // WP Optimize
73 add_filter('wp-optimize-minify-default-exclusions', [self::class, 'appendExclusionsArray']);
74
75 // W3 Total Cache (matches the tag and file path; this filter has no handle)
76 add_filter('w3tc_minify_js_do_tag_minification', [self::class, 'denyTagMinification'], 10, 3);
77
78 // Hummingbird, at 20 so it runs after Hummingbird's own settings filters at 10
79 add_filter('wphb_minify_resource', [self::class, 'denyForBitformHandle'], 20, 2);
80 add_filter('wphb_combine_resource', [self::class, 'denyForBitformHandle'], 20, 2);
81 add_filter('wphb_defer_resource', [self::class, 'denyForBitformHandle'], 20, 2);
82 add_filter('wphb_inline_resource', [self::class, 'denyForBitformHandle'], 20, 2);
83
84 // Jetpack Boost, Page Optimize and WordPress.com script concatenation
85 add_filter('js_do_concat', [self::class, 'denyForBitformHandle'], 20, 2);
86
87 // Generic no-optimize attributes on our own script tags
88 add_filter('script_loader_tag', [self::class, 'addNoOptimizeAttributes'], 10, 2);
89 add_filter('wp_inline_script_attributes', [self::class, 'addInlineNoOptimizeAttributes'], 10, 2);
90 }
91
92 /** Register the cache purge that runs after a form changes. */
93 public static function registerPurgeHooks()
94 {
95 add_action('bitform_admin_form_changed', [self::class, 'schedulePurge']);
96 }
97
98 /**
99 * Queue one cache purge for the end of the request.
100 *
101 * Collects the page list now and runs the purge on shutdown. Disabled by the
102 * bitform_purge_caches_on_form_change filter.
103 */
104 public static function schedulePurge()
105 {
106 if (self::$purgeScheduled) {
107 return;
108 }
109 if (!apply_filters('bitform_purge_caches_on_form_change', true)) {
110 return;
111 }
112 self::$purgeScheduled = true;
113 self::$purgePostIds = self::collectFormPageIds();
114 add_action('shutdown', [self::class, 'purgeCaches'], 100);
115 }
116
117 /**
118 * Post IDs that have rendered a Bit Form.
119 *
120 * @return array
121 */
122 private static function collectFormPageIds()
123 {
124 $postIds = [];
125 try {
126 $forms = (new FormModel())->get(['generated_script_page_ids']);
127 if (is_wp_error($forms) || !is_array($forms)) {
128 return [];
129 }
130 foreach ($forms as $form) {
131 $pages = json_decode(isset($form->generated_script_page_ids) ? $form->generated_script_page_ids : '', true);
132 if (!is_array($pages)) {
133 continue;
134 }
135 foreach (array_keys($pages) as $postId) {
136 $postId = absint($postId);
137 if ($postId) {
138 $postIds[] = $postId;
139 }
140 }
141 }
142 } catch (\Throwable $err) {
143 Log::debug_log('Cache purge page lookup failed: ' . $err->getMessage());
144 return [];
145 }
146 return array_values(array_unique($postIds));
147 }
148
149 /**
150 * Purge the cached pages that render a Bit Form.
151 *
152 * Purges page by page, or flushes everything once the page count passes
153 * bitform_cache_purge_page_limit.
154 */
155 public static function purgeCaches()
156 {
157 $postIds = apply_filters('bitform_cache_purge_post_ids', self::$purgePostIds);
158 $postIds = is_array($postIds) ? array_values(array_filter(array_map('absint', $postIds))) : [];
159
160 // No page has rendered a form, so nothing cached can be stale.
161 if (empty($postIds)) {
162 do_action('bitform_purge_caches', [], 'none');
163 return;
164 }
165
166 $limit = (int) apply_filters('bitform_cache_purge_page_limit', 50);
167 if ($limit > 0 && count($postIds) > $limit) {
168 self::purgeEverything();
169 do_action('bitform_purge_caches', $postIds, 'everything');
170 return;
171 }
172
173 $urls = [];
174 foreach ($postIds as $postId) {
175 $url = get_permalink($postId);
176 if (is_string($url) && '' !== $url) {
177 $urls[] = $url;
178 }
179 }
180
181 self::purgePages($postIds, $urls);
182 self::purgeWholeCacheForPluginsWithoutPageApi();
183
184 // $mode is 'pages', 'everything' or 'none'.
185 do_action('bitform_purge_caches', $postIds, 'pages');
186 }
187
188 /**
189 * Per-page purge for every caching plugin that exposes one.
190 *
191 * @param array $postIds
192 * @param array $urls
193 *
194 * @return void
195 */
196 private static function purgePages($postIds, $urls)
197 {
198 // WP Rocket
199 self::tryPurge(function () use ($postIds) {
200 if (function_exists('rocket_clean_post')) {
201 foreach ($postIds as $postId) {
202 \rocket_clean_post($postId);
203 }
204 }
205 });
206
207 // LiteSpeed Cache
208 self::tryPurge(function () use ($postIds) {
209 if (has_action('litespeed_purge_post')) {
210 foreach ($postIds as $postId) {
211 do_action('litespeed_purge_post', $postId);
212 }
213 }
214 });
215
216 // W3 Total Cache
217 self::tryPurge(function () use ($postIds) {
218 if (function_exists('w3tc_flush_post')) {
219 foreach ($postIds as $postId) {
220 \w3tc_flush_post($postId);
221 }
222 }
223 });
224
225 // WP Super Cache
226 self::tryPurge(function () use ($urls) {
227 if (function_exists('wpsc_delete_url_cache')) {
228 foreach ($urls as $url) {
229 \wpsc_delete_url_cache($url);
230 }
231 }
232 });
233
234 // Cache Enabler
235 self::tryPurge(function () use ($postIds) {
236 if (class_exists('\Cache_Enabler')) {
237 foreach ($postIds as $postId) {
238 \Cache_Enabler::clear_page_cache_by_post_id($postId);
239 }
240 }
241 });
242
243 // WP-Optimize
244 self::tryPurge(function () use ($postIds) {
245 if (class_exists('\WPO_Page_Cache')) {
246 foreach ($postIds as $postId) {
247 \WPO_Page_Cache::delete_single_post_cache($postId);
248 }
249 }
250 });
251
252 // SiteGround Optimizer
253 self::tryPurge(function () use ($urls) {
254 if (function_exists('sg_cachepress_purge_cache')) {
255 foreach ($urls as $url) {
256 \sg_cachepress_purge_cache($url);
257 }
258 }
259 });
260
261 // NitroPack
262 self::tryPurge(function () use ($urls) {
263 if (function_exists('nitropack_purge_url')) {
264 foreach ($urls as $url) {
265 \nitropack_purge_url($url);
266 }
267 }
268 });
269
270 // Nginx Helper (server-level FastCGI / Redis page cache)
271 self::tryPurge(function () use ($urls) {
272 if (defined('NGINX_HELPER_BASENAME') || class_exists('Nginx_Helper')) {
273 foreach ($urls as $url) {
274 do_action('rt_nginx_helper_purge_url', $url);
275 }
276 }
277 });
278
279 // Elementor stores rendered document HTML in post meta with a 24h TTL.
280 self::tryPurge(function () use ($postIds) {
281 if (class_exists('\Elementor\Core\Base\Document')) {
282 foreach ($postIds as $postId) {
283 delete_post_meta($postId, \Elementor\Core\Base\Document::CACHE_META_KEY);
284 }
285 }
286 });
287 }
288
289 /**
290 * Full flush for caching plugins that expose no per-page purge API.
291 *
292 * Each call is a no-op unless that plugin is installed. Disabled by the
293 * bitform_cache_full_purge_fallback filter.
294 *
295 * @return void
296 */
297 private static function purgeWholeCacheForPluginsWithoutPageApi()
298 {
299 if (!apply_filters('bitform_cache_full_purge_fallback', true)) {
300 return;
301 }
302
303 // FlyingPress
304 self::tryPurge(function () {
305 if (class_exists('\FlyingPress\Purge')) {
306 \FlyingPress\Purge::purge_everything();
307 }
308 });
309
310 // Breeze
311 self::tryPurge(function () {
312 if (class_exists('\Breeze_PurgeCache')) {
313 \Breeze_PurgeCache::breeze_cache_flush();
314 }
315 });
316
317 // Hummingbird
318 self::tryPurge(function () {
319 if (has_action('wphb_clear_page_cache')) {
320 do_action('wphb_clear_page_cache');
321 }
322 });
323
324 // Swift Performance
325 self::tryPurge(function () {
326 if (class_exists('\Swift_Performance_Cache')) {
327 \Swift_Performance_Cache::clear_all_cache();
328 }
329 });
330
331 // Comet Cache
332 self::tryPurge(function () {
333 if (class_exists('\comet_cache')) {
334 \comet_cache::clear();
335 }
336 });
337
338 // WP Fastest Cache
339 self::tryPurge(function () {
340 if (class_exists('WpFastestCache')) {
341 do_action('wpfc_clear_all_cache', true);
342 }
343 });
344
345 // WP Engine
346 self::tryPurge(function () {
347 if (class_exists('\WpeCommon')) {
348 \WpeCommon::purge_varnish_cache();
349 }
350 });
351 }
352
353 /**
354 * Flush every installed caching plugin's whole cache.
355 *
356 * @return void
357 */
358 private static function purgeEverything()
359 {
360 self::tryPurge(function () {
361 if (function_exists('rocket_clean_domain')) {
362 \rocket_clean_domain();
363 }
364 });
365 self::tryPurge(function () {
366 if (has_action('litespeed_purge_all')) {
367 do_action('litespeed_purge_all');
368 }
369 });
370 self::tryPurge(function () {
371 if (function_exists('w3tc_flush_all')) {
372 \w3tc_flush_all();
373 }
374 });
375 self::tryPurge(function () {
376 if (function_exists('wp_cache_clear_cache')) {
377 is_multisite() ? \wp_cache_clear_cache(get_current_blog_id()) : \wp_cache_clear_cache();
378 }
379 });
380 self::tryPurge(function () {
381 if (class_exists('\Cache_Enabler')) {
382 \Cache_Enabler::clear_complete_cache();
383 }
384 });
385 self::tryPurge(function () {
386 if (class_exists('\WPO_Page_Cache')) {
387 \WPO_Page_Cache::instance()->purge();
388 }
389 });
390 self::tryPurge(function () {
391 if (function_exists('sg_cachepress_purge_everything')) {
392 \sg_cachepress_purge_everything();
393 }
394 });
395 self::tryPurge(function () {
396 if (function_exists('nitropack_purge')) {
397 \nitropack_purge();
398 }
399 });
400 self::tryPurge(function () {
401 if (defined('NGINX_HELPER_BASENAME') || class_exists('Nginx_Helper')) {
402 do_action('rt_nginx_helper_purge_all');
403 }
404 });
405 // Elementor's document cache, CSS files and asset data.
406 self::tryPurge(function () {
407 if (class_exists('\Elementor\Plugin')) {
408 \Elementor\Plugin::$instance->files_manager->clear_cache();
409 }
410 });
411
412 self::purgeWholeCacheForPluginsWithoutPageApi();
413 }
414
415 /**
416 * Run one purge, swallowing third-party failures.
417 *
418 * @param callable $purge
419 *
420 * @return void
421 */
422 private static function tryPurge($purge)
423 {
424 try {
425 $purge();
426 } catch (\Throwable $err) {
427 Log::debug_log('Cache purge failed: ' . $err->getMessage());
428 }
429 }
430
431 /**
432 * Substrings optimizers match against a script's URL, handle or inline body.
433 *
434 * @return array
435 */
436 public static function exclusionPatterns()
437 {
438 $patterns = [
439 'bf_globals', // inline config content
440 'bitform', // handles + generated file names (bitform-js-*, bitforms_*)
441 'bitforms/form-scripts', // uploads path of the generated runtime bundle
442 'bit-form-all-script', // runtime bundle handle
443 ];
444 $filtered = apply_filters('bitform_cache_exclusion_patterns', $patterns);
445 return is_array($filtered) ? $filtered : $patterns;
446 }
447
448 /**
449 * Append patterns to an array-based optimizer filter. Typed loosely because
450 * optimizers pass mixed shapes through these filters.
451 *
452 * @param mixed $exclusions
453 *
454 * @return mixed
455 */
456 public static function appendExclusionsArray($exclusions)
457 {
458 if (!is_array($exclusions)) {
459 return $exclusions;
460 }
461 return array_values(array_unique(array_merge($exclusions, self::exclusionPatterns())));
462 }
463
464 /**
465 * Append exclusion patterns to a comma-separated string filter (Autoptimize).
466 *
467 * @param mixed $exclusions
468 *
469 * @return mixed
470 */
471 public static function appendExclusionsString($exclusions)
472 {
473 if (!is_string($exclusions)) {
474 return $exclusions;
475 }
476 $parts = array_filter(array_map('trim', explode(',', $exclusions)));
477 $parts = array_unique(array_merge($parts, self::exclusionPatterns()));
478 return implode(',', $parts);
479 }
480
481 /**
482 * Append script handles to a handle-based filter (SiteGround).
483 *
484 * SiteGround matches exact handle names and our per-form inline handles are
485 * dynamic (bitform-bf-globals-bitforms_15_123_1), so scan the registered
486 * queue rather than passing prefixes, which it would not match.
487 *
488 * @param mixed $handles
489 *
490 * @return mixed
491 */
492 public static function appendHandles($handles)
493 {
494 if (!is_array($handles)) {
495 return $handles;
496 }
497 $bitformHandles = self::HANDLE_PREFIXES;
498 $wpScripts = wp_scripts();
499 if (!empty($wpScripts->registered)) {
500 foreach (array_keys($wpScripts->registered) as $registeredHandle) {
501 if (self::isBitformHandle($registeredHandle)) {
502 $bitformHandles[] = $registeredHandle;
503 }
504 }
505 }
506 return array_values(array_unique(array_merge($handles, $bitformHandles)));
507 }
508
509 /**
510 * Whether a script handle belongs to Bit Form's frontend runtime.
511 *
512 * @param mixed $handle
513 *
514 * @return bool
515 */
516 private static function isBitformHandle($handle)
517 {
518 if (!is_string($handle) || '' === $handle) {
519 return false;
520 }
521 foreach (self::HANDLE_PREFIXES as $prefix) {
522 if (0 === strpos($handle, $prefix)) {
523 return true;
524 }
525 }
526 return false;
527 }
528
529 /**
530 * Whether a tag or file path carries one of the exclusion patterns.
531 *
532 * @param mixed $value
533 *
534 * @return bool
535 */
536 private static function matchesExclusionPattern($value)
537 {
538 if (!is_string($value) || '' === $value) {
539 return false;
540 }
541 foreach (self::exclusionPatterns() as $pattern) {
542 if (is_string($pattern) && '' !== $pattern && false !== strpos($value, $pattern)) {
543 return true;
544 }
545 }
546 return false;
547 }
548
549 /**
550 * Return false for Bit Form handles on boolean, handle-keyed optimizer
551 * filters (Hummingbird's minify/combine/defer/inline, js_do_concat).
552 *
553 * @param mixed $value
554 * @param mixed $handle
555 *
556 * @return mixed
557 */
558 public static function denyForBitformHandle($value, $handle = '')
559 {
560 return self::isBitformHandle($handle) ? false : $value;
561 }
562
563 /**
564 * Deny W3 Total Cache tag minification for Bit Form scripts.
565 *
566 * @param mixed $doMinification
567 * @param mixed $scriptTag
568 * @param mixed $file
569 *
570 * @return mixed
571 */
572 public static function denyTagMinification($doMinification, $scriptTag = '', $file = '')
573 {
574 if (self::matchesExclusionPattern($scriptTag) || self::matchesExclusionPattern($file)) {
575 return false;
576 }
577 return $doMinification;
578 }
579
580 /**
581 * Stamp exclusion attributes on Bit Form <script src> tags. Honored by
582 * Cloudflare Rocket Loader, WP Rocket, LiteSpeed, FlyingPress, Breeze,
583 * NitroPack (nitro-exclude) and Jetpack Boost (data-jetpack-boost).
584 *
585 * @param mixed $tag
586 * @param mixed $handle
587 *
588 * @return mixed
589 */
590 public static function addNoOptimizeAttributes($tag, $handle)
591 {
592 if (!is_string($tag) || !self::isBitformHandle($handle)) {
593 return $tag;
594 }
595 if (false !== strpos($tag, 'data-no-optimize')) {
596 return $tag;
597 }
598 return str_replace(
599 '<script ',
600 '<script data-no-optimize="1" data-no-defer="1" data-no-minify="1" data-cfasync="false" data-jetpack-boost="ignore" nowprocket nitro-exclude ',
601 $tag
602 );
603 }
604
605 /**
606 * Same attributes for wp_add_inline_script fragments, which never pass
607 * through script_loader_tag. WP 6.3+.
608 *
609 * @param mixed $attributes
610 * @param mixed $data
611 *
612 * @return mixed
613 */
614 public static function addInlineNoOptimizeAttributes($attributes, $data = '')
615 {
616 if (!is_array($attributes)) {
617 return $attributes;
618 }
619 $id = isset($attributes['id']) ? $attributes['id'] : '';
620 $isBitformInline = (is_string($id) && 0 === strpos($id, 'bitform'))
621 || (is_string($data) && false !== strpos($data, 'bf_globals'));
622 if ($isBitformInline) {
623 $attributes['data-no-optimize'] = '1';
624 $attributes['data-no-defer'] = '1';
625 $attributes['data-no-minify'] = '1';
626 $attributes['data-cfasync'] = 'false';
627 $attributes['data-jetpack-boost'] = 'ignore';
628 $attributes['nowprocket'] = true;
629 $attributes['nitro-exclude'] = true;
630 }
631 return $attributes;
632 }
633 }
634