PluginProbe
StoreEngine — Complete eCommerce Solution with Memberships, Licensing, Affiliates & More / 2.0.0
StoreEngine — Complete eCommerce Solution with Memberships, Licensing, Affiliates & More v2.0.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.0.0, at includes/utils/caching.php

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