GravityView  2.17
The best, easiest way to display Gravity Forms entries on your website.
class-cache.php
Go to the documentation of this file.
1 <?php
2 
3 /**
4  * Handle caching using transients for GravityView
5  */
7 
8  /** @deprecated 2.14 - use BLOCKLIST_OPTION_NAME instead! */
9  const BLACKLIST_OPTION_NAME = 'gravityview_cache_blocklist';
10 
11  const BLOCKLIST_OPTION_NAME = 'gravityview_cache_blocklist';
12 
13  /**
14  * Form ID, or array of Form IDs
15  *
16  * @var mixed
17  */
18  protected $form_ids;
19 
20  /**
21  * Extra request parameters used to generate the query. This is used to generate the unique transient key.
22  *
23  * @var array
24  */
25  protected $args;
26 
27  /**
28  * The transient key used to store the cached item. 45 characters long.
29  *
30  * @var string
31  */
32  private $key = '';
33 
34  /**
35  * Whether to use the cache or not. Set in {@see use_cache()}.
36  *
37  * @var null|boolean $use_cache
38  */
39  private $use_cache = null;
40 
41  /**
42  *
43  * @param array|int $form_ids Form ID or array of form IDs used in a request
44  * @param array $args Extra request parameters used to generate the query. This is used to generate the unique transient key.
45  */
46  function __construct( $form_ids = NULL, $args = array() ) {
47 
48  $this->add_hooks();
49 
50  if ( ! is_null( $form_ids ) ) {
51 
52  $this->form_ids = $form_ids;
53 
54  $this->args = $args;
55 
56  $this->set_key();
57  }
58  }
59 
60  /**
61  * Add actions for clearing out caches when entries are updated.
62  */
63  function add_hooks() {
64 
65  // Schedule cleanup of expired transients
66  add_action( 'wp', array( $this, 'schedule_transient_cleanup' ) );
67 
68  // Hook in to the scheduled cleanup, if scheduled
69  add_action( 'gravityview-expired-transients', array( $this, 'delete_expired_transients' ) );
70 
71  // Trigger this when you need to prevent any results from being cached with forms that have been modified
72  add_action( 'gravityview_clear_form_cache', array( $this, 'blocklist_add' ) );
73 
74  /**
75  * @since 1.14
76  */
77  add_action( 'gravityview_clear_entry_cache', array( $this, 'entry_property_changed' ) );
78 
79  add_action( 'gform_after_update_entry', array( $this, 'entry_updated' ), 10, 2 );
80 
81  add_action( 'gform_entry_created', array( $this, 'entry_created' ), 10, 2 );
82 
83  add_action( 'gform_post_add_entry', array( $this, 'entry_added' ), 10, 2 );
84 
85  add_action( 'gform_post_update_entry_property', array( $this, 'entry_property_changed' ), 10, 4 );
86 
87  add_action( 'gform_delete_lead', array( $this, 'entry_property_changed' ), 10 );
88 
89  }
90 
91  /**
92  * Force refreshing a cache when an entry is deleted.
93  *
94  * The `gform_delete_lead` action is called before the lead is deleted; we fetch the entry to find out the form ID so it can be added to the blocklist.
95  *
96  * @since 1.5.1
97  *
98  * @param int $lead_id Entry ID
99  * @param string $property_value Previous value of the lead status passed by gform_update_status hook
100  * @param string $previous_value Previous value of the lead status passed by gform_update_status hook
101  *
102  * @return void
103  */
104  public function entry_status_changed( $lead_id, $property_value = '', $previous_value = '' ) {
105 
106  $entry = GFAPI::get_entry( $lead_id );
107 
108  if ( is_wp_error( $entry ) ) {
109 
110  gravityview()->log->error( 'Could not retrieve entry {entry_id} to delete it: {error}', array( 'entry_id' => $lead_id, 'error' => $entry->get_error_message() ) );
111 
112  return;
113  }
114 
115  gravityview()->log->debug( 'adding form {form_id} to blocklist because entry #{lead_id} was deleted', array( 'form_id' => $entry['form_id'], 'entry_id' => $lead_id, 'data' => array( 'value' => $property_value, 'previous' => $previous_value ) ) );
116 
117  $this->blocklist_add( $entry['form_id'] );
118  }
119 
120  /**
121  * Force refreshing a cache when an entry is deleted.
122  *
123  * The `gform_delete_lead` action is called before the lead is deleted; we fetch the entry to find out the form ID so it can be added to the blocklist.
124  *
125  * @since 2.16.3
126  *
127  * @param int $lead_id The Entry ID.
128  * @param string $property_name The property that was updated.
129  * @param string $property_value The new value of the property that was updated.
130  * @param string $previous_value The previous property value before the update.
131  *
132  * @return void
133  */
134  public function entry_property_changed( $lead_id, $property_name = '', $property_value = '', $previous_value = '' ) {
135 
136  $entry = GFAPI::get_entry( $lead_id );
137 
138  if ( is_wp_error( $entry ) ) {
139 
140  gravityview()->log->error( 'Could not retrieve entry {entry_id} during cache clearing: {error}', array(
141  'entry_id' => $lead_id,
142  'error' => $entry->get_error_message()
143  ) );
144 
145  return;
146  }
147 
148  gravityview()->log->debug( 'adding form {form_id} to blocklist because the {property_name} property was updated for entry #{lead_id}', array(
149  'form_id' => $entry['form_id'],
150  'entry_id' => $lead_id,
151  'data' => array(
152  'value' => $property_value,
153  'previous' => $previous_value,
154  'property_name' => $property_name,
155  )
156  ) );
157 
158  $this->blocklist_add( $entry['form_id'] );
159  }
160 
161  /**
162  * When an entry is updated, add the entry's form to the cache blocklist
163  *
164  * @param array $form GF form array
165  * @param int $lead_id Entry ID
166  *
167  * @return void
168  */
169  public function entry_updated( $form, $lead_id ) {
170 
171  gravityview()->log->debug(' adding form {form_id} to blocklist because entry #{entry_id} was updated', array( 'form_id' => $form['id'], 'entry_id' => $lead_id ) );
172 
173  $this->blocklist_add( $form['id'] );
174  }
175 
176  /**
177  * When an entry is created, add the entry's form to the cache blocklist
178  *
179  * We don't want old caches; when an entry is added, we want to clear the cache.
180  *
181  * @param array $entry GF entry array
182  * @param array $form GF form array
183  *
184  * @return void
185  */
186  public function entry_created( $entry, $form ) {
187 
188  gravityview()->log->debug( 'adding form {form_id} to blocklist because entry #{entry_id} was created', array( 'form_id' => $form['id'], 'entry_id' => $entry['id'] ) );
189 
190  $this->blocklist_add( $form['id'] );
191  }
192 
193  /**
194  * Clear the cache when entries are added via GFAPI::add_entry().
195  *
196  * @param array $entry The GF Entry array
197  * @param array $form The GF Form array
198  *
199  * @return void
200  */
201  public function entry_added( $entry, $form ) {
202  if ( is_wp_error( $entry ) ) {
203  return;
204  }
205 
206  gravityview()->log->debug( 'adding form {form_id} to blocklist because entry #{entry_id} was added', array( 'form_id' => $form['id'], 'entry_id' => $entry['id'] ) );
207 
208  $this->blocklist_add( $form['id'] );
209  }
210 
211  /**
212  * Calculate the prefix based on the Form IDs
213  *
214  * @param int|array $form_ids Form IDs to generate prefix for
215  *
216  * @return string Prefix for the cache string used in set_key()
217  */
218  protected function get_cache_key_prefix( $form_ids = NULL ) {
219 
220  if ( is_null( $form_ids ) ) {
222  }
223 
224  // Normally just one form, but supports multiple forms
225  //
226  // Array of IDs 12, 5, 14 would result in `f:12-f:5-f:14`
227  $forms = 'f:' . implode( '-f:', (array) $form_ids );
228 
229  // Prefix for transient keys
230  // Now the prefix would be: `gv-cache-f:12-f:5-f:14-`
231  return 'gv-cache-' . $forms . '-';
232 
233  }
234 
235  /**
236  * Set the transient key based on the form IDs and the arguments passed to the class
237  */
238  protected function set_key() {
239 
240  // Don't set key if no forms have been set.
241  if ( empty( $this->form_ids ) ) {
242  return;
243  }
244 
245  $key = $this->get_cache_key_prefix() . sha1( serialize( $this->args ) );
246 
247  // The transient name column can handle up to 64 characters.
248  // The `_transient_timeout_` prefix that is prepended to the string is 11 characters.
249  // 64 - 19 = 45
250  // We make sure the key isn't too long or else WP doesn't store data.
251  $this->key = substr( $key, 0, 45 );
252  }
253 
254  /**
255  * Allow public access to get transient key
256  *
257  * @return string Transient key
258  */
259  public function get_key() {
260  return $this->key;
261  }
262 
263  /**
264  * Get the blocklist array.
265  *
266  * @since 2.16.3
267  *
268  * @return array
269  */
270  private function blocklist_get() {
271  $blocklist = get_option( self::BLOCKLIST_OPTION_NAME, array() );
272 
273  return array_map( 'intval', (array) $blocklist );
274  }
275 
276  /**
277  * Add form IDs to a "blocklist" to force the cache to be refreshed
278  *
279  * @param int|array $form_ids Form IDs to force to be updated
280  *
281  * @return boolean False if value was not updated and true if value was updated.
282  */
283  public function blocklist_add( $form_ids ) {
284 
285  $blocklist = $this->blocklist_get();
286 
287  $form_ids = is_array( $form_ids ) ? $form_ids : array( $form_ids );
288 
289  $form_ids = array_map( 'intval', $form_ids );
290 
291  // Add the passed form IDs
292  $blocklist = array_merge( (array) $blocklist, $form_ids );
293 
294  // Don't duplicate
295  $blocklist = array_unique( $blocklist );
296 
297  // Remove empty items from blocklist
298  $blocklist = array_filter( $blocklist );
299 
300  $updated = update_option( self::BLOCKLIST_OPTION_NAME, $blocklist );
301 
302  if ( false !== $updated ) {
303  gravityview()->log->debug( 'Added form IDs to cache blocklist', array( 'data' => array(
304  '$form_ids' => $form_ids,
305  '$blocklist' => $blocklist
306  ) ) );
307  }
308 
309  return $updated;
310  }
311 
312  /**
313  * @deprecated 2.14 {@see GravityView_Cache::blocklist_add()}
314  *
315  * @param int|array $form_ids Form IDs to force to be updated
316  *
317  * @return bool Whether the removal was successful
318  */
319  public function blacklist_add( $form_ids ) {
320  _deprecated_function( __METHOD__, '2.14', 'GravityView_Cache::blocklist_add()' );
321  return $this->blocklist_remove( $form_ids );
322  }
323 
324  /**
325  * Remove Form IDs from blocklist
326  *
327  * @param int|array $form_ids Form IDs to remove
328  *
329  * @return bool Whether the removal was successful
330  */
331  public function blocklist_remove( $form_ids ) {
332 
333  $blocklist = get_option( self::BLOCKLIST_OPTION_NAME, array() );
334 
335  $updated_list = array_diff( $blocklist, (array) $form_ids );
336 
337  gravityview()->log->debug( 'Removing form IDs from cache blocklist', array( 'data' => array(
338  '$form_ids' => $form_ids,
339  '$blocklist' => $blocklist,
340  '$updated_list' => $updated_list
341  ) ) );
342 
343  return update_option( self::BLOCKLIST_OPTION_NAME, $updated_list );
344  }
345 
346  /**
347  * @deprecated 2.14 {@see GravityView_Cache::blocklist_remove()}
348  *
349  * @param int|array $form_ids Form IDs to add
350  *
351  * @return bool Whether the removal was successful
352  */
353  public function blacklist_remove( $form_ids ) {
354  _deprecated_function( __METHOD__, '2.14', 'GravityView_Cache::blocklist_remove()' );
355  return $this->blocklist_remove( $form_ids );
356  }
357 
358  /**
359  * Is a form ID in the cache blocklist?
360  *
361  * @param int|array $form_ids Form IDs to check if in blocklist
362  *
363  * @deprecated 2.14 Use {@see GravityView_Cache::in_blocklist()}
364  *
365  * @return bool
366  */
367  public function in_blacklist( $form_ids = NULL ) {
368  _deprecated_function( __METHOD__, '2.14', 'GravityView_Cache::in_blocklist()' );
369  return $this->in_blocklist( $form_ids );
370  }
371 
372  /**
373  * Is a form ID in the cache blocklist
374  *
375  * @param int|array $form_ids Form IDs to check if in blocklist
376  *
377  * @return bool
378  */
379  public function in_blocklist( $form_ids = NULL ) {
380 
381  $blocklist = $this->blocklist_get();
382 
383  // Use object var if exists
384  $form_ids = is_null( $form_ids ) ? $this->form_ids : $form_ids;
385 
386  if ( empty( $form_ids ) ) {
387 
388  gravityview()->log->debug( 'Did not add form to blocklist; empty form ID', array( 'data' => $form_ids ) );
389 
390  return false;
391  }
392 
393  foreach ( (array) $form_ids as $form_id ) {
394 
395  if ( in_array( (int) $form_id, $blocklist, true ) ) {
396 
397  gravityview()->log->debug( 'Form #{form_id} is in the cache blocklist', array( 'form_id' => $form_id ) );
398 
399  return true;
400  }
401  }
402 
403  return false;
404  }
405 
406 
407  /**
408  * Get transient result
409  *
410  * @param string $key Transient key to fetch
411  *
412  * @return mixed False: Not using cache or cache was a WP_Error object; NULL: no results found; Mixed: cache value
413  */
414  public function get( $key = null ) {
415 
416  $key = is_null( $key ) ? $this->key : $key;
417 
418  if ( ! $this->use_cache() ) {
419 
420  gravityview()->log->debug( 'Not using cached results because of GravityView_Cache->use_cache() results' );
421 
422  return false;
423  }
424 
425  gravityview()->log->debug( 'Fetching request with transient key {key}', array( 'key' => $key ) );
426 
427  $result = get_transient( $key );
428 
429  if ( is_wp_error( $result ) ) {
430 
431  gravityview()->log->debug( 'Fetching request resulted in error:', array( 'data' => $result ) );
432 
433  return false;
434 
435  } elseif ( $result ) {
436 
437  gravityview()->log->debug( 'Cached results found for transient key {key}', array( 'key' => $key ) );
438 
439  return $result;
440  }
441 
442  gravityview()->log->debug( 'No cached results found for transient key {key}', array( 'key' => $key ) );
443 
444  return NULL;
445  }
446 
447  /**
448  * Cache content as a transient.
449  *
450  * Cache time defaults to 1 day.
451  *
452  * @since 2.16 Added $cache_time parameter to allow overriding the default cache time.
453  *
454  * @param mixed $content The content to cache.
455  * @param string $filter_name Name used to modify the cache time. Will be set to `gravityview_cache_time_{$filter_name}`.
456  * @param int|null $expiration Cache time in seconds. If not set, DAYS_IN_SECONDS will be used.
457  *
458  * @return bool If $content is not set, false. Otherwise, returns true if transient was set and false if not.
459  */
460  public function set( $content, $filter_name = '', $expiration = null ) {
461 
462  // Don't cache empty results
463  if ( ! empty( $content ) ) {
464 
465  $expiration = ! is_int( $expiration ) ? DAY_IN_SECONDS : $expiration;
466 
467  /**
468  * @filter `gravityview_cache_time_{$filter_name}` Modify the cache time for a type of cache
469  * @param int $time_in_seconds Default: `DAY_IN_SECONDS`
470  */
471  $expiration = (int) apply_filters( 'gravityview_cache_time_' . $filter_name, $expiration );
472 
473  gravityview()->log->debug( 'Setting cache with transient key {key} for {expiration} seconds', array( 'key' => $this->key, 'expiration' => $expiration ) );
474 
475  $transient_was_set = set_transient( $this->key, $content, $expiration );
476 
477  if ( ! $transient_was_set && $this->use_cache() ) {
478  gravityview()->log->error( 'Transient was not set for this key: ' . $this->key );
479  }
480 
481  return $transient_was_set;
482  }
483 
484  gravityview()->log->debug( 'Cache not set; content is empty' );
485 
486  return false;
487  }
488 
489  /**
490  * Delete cached transients based on form IDs
491  *
492  * @todo Use REGEX to match forms when array of form IDs is passed, instead of using a simple LIKE
493  * @todo Rate limit deleting to prevent abuse
494  *
495  * @param int|array $form_ids Form IDs to delete
496  *
497  * @return void
498  */
499  public function delete( $form_ids = NULL ) {
500  global $wpdb;
501 
502  // Use object var if exists
503  $form_ids = is_null( $form_ids ) ? $this->form_ids : $form_ids;
504 
505  if ( empty( $form_ids ) ) {
506  gravityview()->log->debug( 'Did not delete cache; empty form IDs' );
507 
508  return;
509  }
510 
511  foreach ( (array) $form_ids as $form_id ) {
512 
513  $key = '_transient_gv-cache-';
514 
515  $key = $wpdb->esc_like( $key );
516 
517  $form_id = intval( $form_id );
518 
519  // Find the transients containing this form
520  $key = "$key%f:$form_id-%"; // \_transient\_gv-cache-%f:1-% for example
521  $sql = $wpdb->prepare( "SELECT option_name FROM {$wpdb->options} WHERE `option_name` LIKE %s", $key );
522 
523  foreach ( ( $transients = $wpdb->get_col( $sql ) ) as $transient ) {
524  // We have to delete it via the API to make sure the object cache is updated appropriately
525  delete_transient( preg_replace( '#^_transient_#', '', $transient ) );
526  }
527 
528  gravityview()->log->debug( 'Deleting cache for form #{form_id}', array( 'form_id' => $form_id, 'data' => array(
529  $sql,
530  sprintf( 'Deleted results: %d', count( $transients ) )
531  ) ) );
532  }
533 
534  }
535 
536  /**
537  * Schedule expired transient cleanup twice a day.
538  *
539  * Can be overruled by the `gravityview_cleanup_transients` filter (returns boolean)
540  *
541  * @return void
542  */
543  public function schedule_transient_cleanup() {
544 
545  /**
546  * @filter `gravityview_cleanup_transients` Override GravityView cleanup of transients by setting this to false
547  * @param boolean $cleanup Whether to run the GravityView auto-cleanup of transients. Default: `true`
548  */
549  $cleanup = apply_filters( 'gravityview_cleanup_transients', true );
550 
551  if ( ! $cleanup ) {
552  return;
553  }
554 
555  if ( ! wp_next_scheduled( 'gravityview-expired-transients' ) ) {
556  wp_schedule_event( time(), 'daily', 'gravityview-expired-transients' );
557  }
558  }
559 
560  /**
561  * Delete expired transients.
562  *
563  * The code is copied from the Delete Expired Transients, with slight modifications to track # of results and to get the blog ID dynamically
564  *
565  * @see https://wordpress.org/plugins/delete-expired-transients/ Plugin where the code was taken from
566  * @see DelxtransCleaners::clearBlogExpired()
567  * @return void
568  */
569  public function delete_expired_transients() {
570  global $wpdb;
571 
572  // Added this line, which isn't in the plugin
573  $blog_id = get_current_blog_id();
574 
575  $num_results = 0;
576 
577  // get current PHP time, offset by a minute to avoid clashes with other tasks
578  $threshold = time() - 60;
579 
580  // get table name for options on specified blog
581  $table = $wpdb->get_blog_prefix( $blog_id ) . 'options';
582 
583  // delete expired transients, using the paired timeout record to find them
584  $sql = "
585  delete from t1, t2
586  using $table t1
587  join $table t2 on t2.option_name = replace(t1.option_name, '_timeout', '')
588  where (t1.option_name like '\_transient\_timeout\_%' or t1.option_name like '\_site\_transient\_timeout\_%')
589  and t1.option_value < '$threshold'
590  ";
591 
592  $num_results = $wpdb->query( $sql );
593 
594  // delete orphaned transient expirations
595  // also delete NextGEN Gallery 2.x display cache timeout aliases
596  $sql = "
597  delete from $table
598  where (
599  option_name like '\_transient\_timeout\_%'
600  or option_name like '\_site\_transient\_timeout\_%'
601  or option_name like 'displayed\_galleries\_%'
602  or option_name like 'displayed\_gallery\_rendering\_%'
603  )
604  and option_value < '$threshold'
605  ";
606 
607  $num_results += $wpdb->query( $sql );
608 
609  gravityview()->log->debug( 'Deleted {count} expired transient records from the database', array( 'count' => $num_results ) );
610  }
611 
612  /**
613  * Check whether to use cached results, if available
614  *
615  * If the user can edit posts, they are able to override whether to cache results by adding `cache` or `nocache` to the URL requested.
616  *
617  * @return boolean True: use cache; False: don't use cache
618  */
619  public function use_cache() {
620 
621  // Exit early if debugging (unless running PHPUnit)
622  if ( defined( 'GRAVITYVIEW_DISABLE_CACHE' ) && GRAVITYVIEW_DISABLE_CACHE && ! ( defined('DOING_GRAVITYVIEW_TESTS' ) && DOING_GRAVITYVIEW_TESTS ) ) {
623  return (boolean) apply_filters( 'gravityview_use_cache', false, $this );
624  }
625 
626  // Only run once per instance.
627  if ( ! is_null( $this->use_cache ) ) {
628  return $this->use_cache;
629  }
630 
631  $use_cache = true;
632 
633  if ( GVCommon::has_cap( 'edit_gravityviews' ) ) {
634 
635  if ( isset( $_GET['cache'] ) || isset( $_GET['nocache'] ) ) {
636 
637  gravityview()->log->debug( 'Not using cache: ?cache or ?nocache is in the URL' );
638 
639  $use_cache = false;
640  }
641 
642  }
643 
644  // Has the form been flagged as having changed items in it?
645  if ( ! $use_cache || $this->in_blocklist() ) {
646 
647  // Delete caches for all items with form IDs XYZ
648  $this->delete( $this->form_ids );
649 
650  // Remove the form from
651  $this->blocklist_remove( $this->form_ids );
652  }
653 
654  /**
655  * @filter `gravityview_use_cache` Modify whether to use the cache or not
656  * @param boolean $use_cache Previous setting
657  * @param GravityView_Cache $this The GravityView_Cache object
658  */
659  $this->use_cache = (boolean) apply_filters( 'gravityview_use_cache', $use_cache, $this );
660 
661  return $this->use_cache;
662  }
663 
664 }
665 
add_hooks()
Add actions for clearing out caches when entries are updated.
Definition: class-cache.php:63
get_key()
Allow public access to get transient key.
blocklist_add( $form_ids)
Add form IDs to a "blocklist" to force the cache to be refreshed.
entry_added( $entry, $form)
Clear the cache when entries are added via GFAPI::add_entry().
$use_cache
Whether to use the cache or not.
Definition: class-cache.php:39
blacklist_add( $form_ids)
$forms
Definition: data-source.php:19
entry_created( $entry, $form)
When an entry is created, add the entry&#39;s form to the cache blocklist.
blocklist_remove( $form_ids)
Remove Form IDs from blocklist.
blocklist_get()
Get the blocklist array.
new GravityView_Cache
get_cache_key_prefix( $form_ids=NULL)
Calculate the prefix based on the Form IDs.
set_key()
Set the transient key based on the form IDs and the arguments passed to the class.
in_blocklist( $form_ids=NULL)
Is a form ID in the cache blocklist.
entry_status_changed( $lead_id, $property_value='', $previous_value='')
Force refreshing a cache when an entry is deleted.
Handle caching using transients for GravityView.
Definition: class-cache.php:6
if(gravityview() ->plugin->is_GF_25()) $form
entry_updated( $form, $lead_id)
When an entry is updated, add the entry&#39;s form to the cache blocklist.
const BLACKLIST_OPTION_NAME
Definition: class-cache.php:9
if(empty( $field_settings['content'])) $content
Definition: custom.php:37
schedule_transient_cleanup()
Schedule expired transient cleanup twice a day.
blacklist_remove( $form_ids)
__construct( $form_ids=NULL, $args=array())
Definition: class-cache.php:46
delete_expired_transients()
Delete expired transients.
entry_property_changed( $lead_id, $property_name='', $property_value='', $previous_value='')
Force refreshing a cache when an entry is deleted.
if(empty( $created_by)) $form_id
gravityview()
The main GravityView wrapper function.
static has_cap( $caps='', $object_id=null, $user_id=null)
Alias of GravityView_Roles_Capabilities::has_cap()
$entry
Definition: notes.php:27
use_cache()
Check whether to use cached results, if available.
const BLOCKLIST_OPTION_NAME
Definition: class-cache.php:11
in_blacklist( $form_ids=NULL)
Is a form ID in the cache blocklist?