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