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