PluginProbe
StoreEngine — Complete eCommerce Solution with Memberships, Licensing, Affiliates & More / 2.2.0
StoreEngine — Complete eCommerce Solution with Memberships, Licensing, Affiliates & More v2.2.0
2.3.0 2.2.0 2.1.1 2.1.0 2.0.0 1.10.0 1.9.1 1.9.0 1.2.1 1.2.2 1.3.0 1.3.1 1.3.2 1.3.3 1.4.0 1.5.0 1.5.1 1.5.2 1.5.3 1.5.4 1.5.5 1.5.6 1.5.7 1.5.8 1.6.0 All 59 releases
storeengine / includes / utils / caching.php

caching.php in StoreEngine — Complete eCommerce Solution with Memberships, Licensing, Affiliates & More 2.2.0, at includes/utils/caching.php

410 lines 13.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Caching utilities
4 */
5
6 namespace StoreEngine\Utils;
7
8 use StoreEngine\Classes\Customer;
9
10 if ( ! defined( 'ABSPATH' ) ) {
11 exit;
12 }
13
14 class Caching {
15 /**
16 * Transients to delete on shutdown.
17 *
18 * @var array Array of transient keys.
19 */
20 private static array $delete_transients = [];
21
22 public static function init() {
23 add_filter( 'nocache_headers', [ __CLASS__, 'additional_nocache_headers' ], 10 );
24 add_action( 'shutdown', [ __CLASS__, 'delete_transients_on_shutdown' ], 10 );
25 add_action( 'template_redirect', [ __CLASS__, 'geolocation_ajax_redirect' ] );
26 add_action( 'storeengine/update_checkout', [ __CLASS__, 'update_geolocation_hash' ], 5 );
27 add_action( 'admin_notices', [ __CLASS__, 'notices' ] );
28 add_action( 'delete_version_transients', [ __CLASS__, 'delete_version_transients' ], 10 );
29 add_action( 'wp', [ __CLASS__, 'prevent_caching' ] );
30 add_action( 'clean_term_cache', [ __CLASS__, 'clean_term_cache' ], 10, 2 );
31 add_action( 'edit_terms', [ __CLASS__, 'clean_term_cache' ], 10, 2 );
32 }
33
34 /**
35 * Set additional nocache headers.
36 *
37 * @param array $headers Header names and field values.
38 */
39 public static function additional_nocache_headers( array $headers ): array {
40 global $wp_query;
41
42 $agent = isset( $_SERVER['HTTP_USER_AGENT'] ) ? wp_unslash( $_SERVER['HTTP_USER_AGENT'] ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
43
44 $set_cache = false;
45
46 /**
47 * Allow plugins to enable nocache headers. Enabled for Google weblight.
48 *
49 * @param bool $enable_nocache_headers Flag indicating whether to add nocache headers. Default: false.
50 */
51 if ( apply_filters( 'storeengine/enable_nocache_headers', false ) ) {
52 $set_cache = true;
53 }
54
55 /**
56 * Enabled for Google weblight.
57 *
58 * @see https://support.google.com/webmasters/answer/1061943?hl=en
59 */
60 if ( false !== strpos( $agent, 'googleweblight' ) ) {
61 // no-transform: Opt-out of Google weblight. https://support.google.com/webmasters/answer/6211428?hl=en.
62 $set_cache = true;
63 }
64
65 // Chrome (and other Chromium browsers) aggressively back/forward-cache
66 // (bfcache) pages without `no-store`. On the checkout that means a stale
67 // page is restored after the add-to-cart → checkout redirect and the
68 // Stripe Elements iframe never re-initialises (a hard refresh / cache
69 // clear "fixes" it). Cart AND checkout must both send `no-store` — this
70 // mirrors the conventional storefront behaviour, whose `is_cart() || is_checkout()` check was
71 // ported here without the checkout half.
72 if ( false !== strpos( $agent, 'Chrome' ) && isset( $wp_query ) && ( Helper::is_cart() || Helper::is_checkout() ) ) {
73 $set_cache = true;
74 }
75
76 if ( $set_cache ) {
77 $headers['Cache-Control'] = 'no-transform, no-cache, no-store, must-revalidate';
78 }
79
80 return $headers;
81 }
82
83 /**
84 * Add a transient to delete on shutdown.
85 *
86 * @param string|array $keys Transient key or keys.
87 */
88 public static function queue_delete_transient( $keys ) {
89 self::$delete_transients = array_unique( array_merge( is_array( $keys ) ? $keys : array( $keys ), self::$delete_transients ) );
90 }
91
92 /**
93 * Transients that don't need to be cleaned right away can be deleted on shutdown to avoid repetition.
94 */
95 public static function delete_transients_on_shutdown() {
96 if ( self::$delete_transients ) {
97 foreach ( self::$delete_transients as $key ) {
98 delete_transient( $key );
99 }
100 self::$delete_transients = array();
101 }
102 }
103
104 /**
105 * Used to clear layered nav counts based on passed attribute names.
106 *
107 * @param array $attribute_keys Attribute keys.
108 */
109 public static function invalidate_attribute_count( array $attribute_keys ) {
110 if ( $attribute_keys ) {
111 foreach ( $attribute_keys as $attribute_key ) {
112 self::queue_delete_transient( 'storeengine_layered_nav_counts_' . $attribute_key );
113 }
114 }
115 }
116
117 /**
118 * Get a hash of the customer location.
119 *
120 * @return string
121 */
122 public static function geolocation_ajax_get_location_hash(): string {
123 $customer = Helper::get_customer( null, true );
124 $location = [
125 'country' => $customer->get_billing_country(),
126 'state' => $customer->get_billing_state(),
127 'postcode' => $customer->get_billing_postcode(),
128 'city' => $customer->get_billing_city(),
129 ];
130 $location_hash = substr( md5( strtolower( implode( '', $location ) ) ), 0, 12 );
131
132 /**
133 * Controls the location hash used in geolocation-based caching.
134 *
135 * @param string $location_hash The hash used for geolocation.
136 * @param array $location The location/address data.
137 * @param Customer $customer The current customer object.
138 */
139 return apply_filters( 'storeengine/geolocation_ajax_get_location_hash', $location_hash, $location, $customer );
140 }
141
142 /**
143 * Prevent caching on certain pages
144 */
145 public static function prevent_caching() {
146 if ( ! is_blog_installed() ) {
147 return;
148 }
149 $page_ids = array_filter( [
150 (int) Helper::get_settings( 'cart_page' ),
151 (int) Helper::get_settings( 'checkout_page' ),
152 (int) Helper::get_settings( 'dashboard_page' ),
153 ] );
154
155 if ( is_page( $page_ids ) ) {
156 self::nocache_headers();
157 }
158 }
159
160 /**
161 * Wrapper for nocache_headers which also disables page caching.
162 */
163 public static function nocache_headers() {
164 self::set_nocache_constants();
165 nocache_headers();
166 }
167
168 /**
169 * When using geolocation via ajax, to bust cache, redirect if the location hash does not equal the querystring.
170 *
171 * This prevents caching of the wrong data for this request.
172 */
173 public static function geolocation_ajax_redirect() {
174 if (
175 Geolocation::is_geolocation_enabled() && Helper::get_settings( 'enable_caching_support' ) &&
176 ! Helper::is_checkout() && ! Helper::is_cart() && ! Helper::is_account_page() &&
177 ! wp_doing_ajax() && empty( $_POST ) // phpcs:ignore WordPress.Security.NonceVerification.Missing
178 ) {
179 $location_hash = self::geolocation_ajax_get_location_hash();
180 $current_hash = isset( $_GET['se-v'] ) ? sanitize_text_field( wp_unslash( $_GET['se-v'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
181
182 if ( empty( $current_hash ) || $current_hash !== $location_hash ) {
183 global $wp;
184
185 $redirect_url = trailingslashit( home_url( $wp->request ) );
186
187 if ( ! empty( $_SERVER['QUERY_STRING'] ) ) {
188 $redirect_url = add_query_arg( wp_unslash( $_SERVER['QUERY_STRING'] ), '', $redirect_url ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
189 }
190
191 if ( ! get_option( 'permalink_structure' ) ) {
192 $redirect_url = add_query_arg( $wp->query_string, '', $redirect_url );
193 }
194
195 $redirect_url = remove_query_arg( [ 'se-v', 'add-to-cart' ], $redirect_url );
196 $redirect_url = add_query_arg( 'se-v', $location_hash, $redirect_url );
197
198 wp_safe_redirect( esc_url_raw( $redirect_url ), 307 );
199 exit;
200 }
201 }
202 }
203
204 /**
205 * Updates the `storeengine_geo_hash` cookie, which is used to help ensure we display
206 * the correct pricing etc. to customers, according to their billing country.
207 *
208 * Note that:
209 *
210 * A) This only sets the cookie if the default customer address is set to "GeoLocate (with
211 * Page Caching Support)".
212 *
213 * B) It is hooked into the order-review update action, which has the benefit of
214 * ensuring we update the cookie any time the billing country is changed.
215 */
216 public static function update_geolocation_hash() {
217 if ( Geolocation::is_geolocation_enabled() && Helper::get_settings( 'enable_caching_support' ) ) {
218 Helper::setcookie( 'storeengine_geo_hash', static::geolocation_ajax_get_location_hash(), time() + HOUR_IN_SECONDS );
219 }
220 }
221
222 /**
223 * Get transient version.
224 *
225 * When using transients with unpredictable names, e.g. those containing a md5
226 * hash in the name, we need a way to invalidate them all at once.
227 *
228 * When using default WP transients we're able to do this with a DB query to
229 * delete transients manually.
230 *
231 * With external cache however, this isn't possible. Instead, this function is used
232 * to append a unique string (based on time()) to each transient. When transients
233 * are invalidated, the transient version will increment and data will be regenerated.
234 *
235 * Adapted from ideas in http://tollmanz.com/invalidation-schemes/.
236 *
237 * @param string $group Name for the group of transients we need to invalidate.
238 * @param boolean $refresh true to force a new version.
239 *
240 * @return string transient version based on time(), 10 digits.
241 */
242 public static function get_transient_version( string $group, bool $refresh = false ): string {
243 $transient_name = $group . '-transient-version';
244 $transient_value = get_transient( $transient_name );
245
246 if ( false === $transient_value || true === $refresh ) {
247 $transient_value = (string) time();
248
249 set_transient( $transient_name, $transient_value );
250 }
251
252 return $transient_value;
253 }
254
255 /**
256 * Set constants to prevent caching by some plugins.
257 *
258 * @param mixed $return Value to return. Previously hooked into a filter.
259 *
260 * @return mixed
261 */
262 public static function set_nocache_constants( $return = true ) {
263 if ( ! defined( 'DONOTCACHEPAGE' ) ) {
264 // Play nice with WP-Super-Cache.
265 define( 'DONOTCACHEPAGE', true ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedConstantFound
266 }
267 if ( ! defined( 'DONOTCACHEOBJECT' ) ) {
268 define( 'DONOTCACHEOBJECT', true ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedConstantFound
269 }
270 if ( ! defined( 'DONOTCACHEDB' ) ) {
271 define( 'DONOTCACHEDB', true ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedConstantFound
272 }
273
274 return $return;
275 }
276
277 /**
278 * Notices function.
279 */
280 public static function notices() {
281 if ( ! function_exists( 'w3tc_pgcache_flush' ) || ! function_exists( 'w3_instance' ) ) {
282 return;
283 }
284
285 /** @noinspection PhpUndefinedFunctionInspection */
286 $config = w3_instance( 'W3_Config' );
287 $enabled = $config->get_integer( 'dbcache.enabled' );
288 $settings = array_map( 'trim', $config->get_array( 'dbcache.reject.sql' ) );
289
290 if ( $enabled && ! in_array( '_storeengine_session_', $settings, true ) ) {
291 ?>
292 <div class="error">
293 <p>
294 <?php
295 /** @noinspection HtmlUnknownTarget */
296 /* translators: 1: key 2: URL */
297 echo wp_kses_post( sprintf( __( 'In order for <strong>database caching</strong> to work with StoreEngine you must add %1$s to the "Ignored Query Strings" option in <a href="%2$s">W3 Total Cache settings</a>.', 'storeengine' ), '<code>_storeengine_session_</code>', esc_url( admin_url( 'admin.php?page=w3tc_dbcache' ) ) ) );
298 ?>
299 </p>
300 </div>
301 <?php
302 }
303 }
304
305 /**
306 * Clean term caches.
307 *
308 * @param array|int $ids Array of ids or single ID to clear cache for.
309 * @param string $taxonomy Taxonomy name.
310 */
311 public static function clean_term_cache( $ids, string $taxonomy ) {
312 if ( Helper::PRODUCT_CATEGORY_TAXONOMY === $taxonomy ) {
313 $ids = is_array( $ids ) ? $ids : array( $ids );
314
315 $clear_ids = array( 0 );
316
317 foreach ( $ids as $id ) {
318 $clear_ids[] = $id;
319 $clear_ids = array_merge( $clear_ids, get_ancestors( $id, Helper::PRODUCT_CATEGORY_TAXONOMY, 'taxonomy' ) );
320 }
321
322 $clear_ids = array_unique( $clear_ids );
323
324 foreach ( $clear_ids as $id ) {
325 wp_cache_delete( 'product-category-hierarchy-' . $id, 'product_cat' );
326 }
327 }
328 }
329
330 /**
331 * When the transient version increases, this is used to remove all past transients to avoid filling the DB.
332 *
333 * Note; this only works on transients appended with the transient version, and when object caching is not being used.
334 *
335 * @param string $version Version of the transient to remove.
336 *
337 * @deprecated 3.6.0 Adjusted transient usage to include versions within the transient values, making this cleanup obsolete.
338 */
339 public static function delete_version_transients( string $version = '' ) {
340 if ( ! wp_using_ext_object_cache() && ! empty( $version ) ) {
341 global $wpdb;
342
343 $limit = apply_filters( 'storeengine/delete_version_transients_limit', 1000 );
344
345 if ( ! $limit ) {
346 return;
347 }
348
349 // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
350 $affected = $wpdb->query( $wpdb->prepare( "DELETE FROM $wpdb->options WHERE option_name LIKE %s LIMIT %d;", '\_transient\_%' . $version, $limit ) ); // : cache ok, db call ok.
351 // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
352
353 // If affected rows is equal to limit, there are more rows to delete. Delete in 30 secs.
354 if ( $affected === $limit ) {
355 wp_schedule_single_event( time() + 30, 'delete_version_transients', array( $version ) );
356 }
357 }
358 }
359
360 /**
361 * Get prefix for use with wp_cache_set. Allows all cache in a group to be invalidated at once.
362 *
363 * @param string $group Group of cache to get.
364 *
365 * @return string Prefix.
366 */
367 public static function get_cache_prefix( string $group ): string {
368 // Get cache key - uses cache key storeengine_orders_cache_prefix to invalidate when needed.
369 $prefix = wp_cache_get( 'storeengine_' . $group . '_cache_prefix', $group );
370
371 if ( false === $prefix ) {
372 $prefix = microtime();
373 wp_cache_set( 'storeengine_' . $group . '_cache_prefix', $prefix, $group );
374 }
375
376 return 'storeengine_cache_' . $prefix . '_';
377 }
378
379 /**
380 * Invalidate cache group.
381 *
382 * @param string $group Group of cache to clear.
383 */
384 public static function invalidate_cache_group( string $group ): bool {
385 return wp_cache_set( 'storeengine_' . $group . '_cache_prefix', microtime(), $group );
386 }
387
388 /**
389 * Helper method to get prefixed key.
390 *
391 * @param string|int $key Key to prefix.
392 * @param string $group Group of cache to get.
393 *
394 * @return string Prefixed key.
395 */
396 public static function get_prefixed_key( $key, string $group ): string {
397 return self::get_cache_prefix( $group ) . $key;
398 }
399
400 public static function get_query_cache_key( $group, $sql, ...$args): string {
401 $key = md5( maybe_serialize( $args ) . $sql );
402
403 $last_changed = wp_cache_get_last_changed( $group );
404
405 return ":$key:$last_changed";
406 }
407 }
408
409 // End of file caching.php.
410