Completed
Push — develop ( 964e49...6d1660 )
by Zack
16:48
created

GravityView_Cache::blacklist_remove()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 14

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
cc 1
nc 1
nop 1
dl 0
loc 14
ccs 0
cts 8
cp 0
crap 2
rs 9.7998
c 0
b 0
f 0
1
<?php
2
3
/**
4
 * Handle caching using transients for GravityView
5
 */
6
class GravityView_Cache {
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 2
	function __construct( $form_ids = NULL, $args = array() ) {
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
43
44 2
		$this->add_hooks();
45
46 2
		if ( ! is_null( $form_ids ) ) {
47
48 2
			$this->form_ids = $form_ids;
49
50 2
			$this->args = $args;
51
52 2
			$this->set_key();
53
		}
54 2
	}
55
56
	/**
57
	 * Add actions for clearing out caches when entries are updated.
58
	 */
59 2
	function add_hooks() {
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
60
61
		// Schedule cleanup of expired transients
62 2
		add_action( 'wp', array( $this, 'schedule_transient_cleanup' ) );
63
64
		// Hook in to the scheduled cleanup, if scheduled
65 2
		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 2
		add_action( 'gravityview_clear_form_cache', array( $this, 'blacklist_add' ) );
69
70
		/**
71
		 * @since 1.14
72
		 */
73 2
		add_action( 'gravityview_clear_entry_cache', array( $this, 'entry_status_changed' ) );
74
75 2
		add_action( 'gform_after_update_entry', array( $this, 'entry_updated' ), 10, 2 );
76
77 2
		add_action( 'gform_entry_created', array( $this, 'entry_created' ), 10, 2 );
78
79 2
		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 2
		foreach( $this->lead_db_columns as $column ) {
85 2
			add_action( 'gform_update_' . $column, array( $this, 'entry_status_changed' ), 10, 3 );
86
		}
87
88 2
		add_action( 'gform_delete_lead', array( $this, 'entry_status_changed' ), 10 );
89 2
	}
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 1
	public function entry_status_changed( $lead_id, $property_value = '', $previous_value = '' ) {
105
106 1
		$entry = GFAPI::get_entry( $lead_id );
107
108 1
		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 1
		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 1
		$this->blacklist_add( $entry['form_id'] );
118 1
	}
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 21
	public function entry_updated( $form, $lead_id ) {
129
130 21
		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 21
		$this->blacklist_add( $form['id'] );
133 21
	}
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 163
	public function entry_added( $entry, $form ) {
161 163
		if ( is_wp_error( $entry ) ) {
162
			return;
163
		}
164
165 163
		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 163
		$this->blacklist_add( $form['id'] );
168 163
	}
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 2
	protected function get_cache_key_prefix( $form_ids = NULL ) {
178
179 2
		if ( is_null( $form_ids ) ) {
180 2
			$form_ids = $this->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 2
		$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 2
		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 2
	protected function set_key() {
198
199
		// Don't set key if no forms have been set.
200 2
		if ( empty( $this->form_ids ) ) {
201
			return;
202
		}
203
204 2
		$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 2
		$this->key = substr( $key, 0, 45 );
211 2
	}
212
213
	/**
214
	 * Allow public access to get transient key
215
	 *
216
	 * @return string Transient key
217
	 */
218 2
	public function get_key() {
219 2
		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 163
	public function blacklist_add( $form_ids ) {
232
233 163
		$blacklist = get_option( self::BLACKLIST_OPTION_NAME, array() );
234
235 163
		$form_ids = is_array( $form_ids ) ? $form_ids : array( $form_ids );
236
237 163
		gravityview()->log->debug( 'Adding form IDs to cache blacklist', array( 'data' => array(
238 163
			'$form_ids'  => $form_ids,
239 163
			'$blacklist' => $blacklist
240
		) ) );
241
242
		// Add the passed form IDs
243 163
		$blacklist = array_merge( (array) $blacklist, $form_ids );
244
245
		// Don't duplicate
246 163
		$blacklist = array_unique( $blacklist );
247
248
		// Remove empty items from blacklist
249 163
		$blacklist = array_filter( $blacklist );
250
251 163
		return update_option( self::BLACKLIST_OPTION_NAME, $blacklist );
252
253
	}
254
255
	/**
256
	 * Remove Form IDs from blacklist
257
	 *
258
	 * @param  int|array $form_ids Form IDs to add
259
	 *
260
	 * @return boolean           Whether the removal was successful
261
	 */
262
	public function blacklist_remove( $form_ids ) {
263
264
		$blacklist = get_option( self::BLACKLIST_OPTION_NAME, array() );
265
266
		$updated_list = array_diff( $blacklist, (array) $form_ids );
267
268
		gravityview()->log->debug( 'Removing form IDs from cache blacklist', array( 'data' => array(
269
			'$form_ids'     => $form_ids,
270
			'$blacklist'    => $blacklist,
271
			'$updated_list' => $updated_list
272
		) ) );
273
274
		return update_option( self::BLACKLIST_OPTION_NAME, $updated_list );
275
	}
276
277
278
	/**
279
	 * Is a form ID in the cache blacklist
280
	 *
281
	 * @param  int|array $form_ids Form IDs to check if in blacklist
282
	 *
283
	 * @return bool
284
	 */
285 3
	function in_blacklist( $form_ids = NULL ) {
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
286
287 3
		$blacklist = get_option( self::BLACKLIST_OPTION_NAME, array() );
288
289
		// Use object var if exists
290 3
		$form_ids = is_null( $form_ids ) ? $this->form_ids : $form_ids;
291
292 3
		if ( empty( $form_ids ) ) {
293
294
			gravityview()->log->debug( 'Did not add form to blacklist; empty form ID', array( 'data' => $form_ids ) );
295
296
			return false;
297
		}
298
299 3
		foreach ( (array) $form_ids as $form_id ) {
300
301 3
			if ( in_array( $form_id, $blacklist ) ) {
302
303 1
				gravityview()->log->debug( 'Form #{form_id} is in the cache blacklist', array( 'form_id' => $form_id ) );
304
305 1
				return true;
306
			}
307
		}
308
309 3
		return false;
310
	}
311
312
313
	/**
314
	 * Get transient result
315
	 *
316
	 * @param  string $key Transient key to fetch
317
	 *
318
	 * @return mixed      False: Not using cache or cache was a WP_Error object; NULL: no results found; Mixed: cache value
319
	 */
320 2
	public function get( $key = NULL ) {
321
322 2
		$key = is_null( $key ) ? $this->key : $key;
323
324 2
		if ( ! $this->use_cache() ) {
325
326
			gravityview()->log->debug( 'Not using cached results because of GravityView_Cache->use_cache() results' );
327
328
			return false;
329
		}
330
331 2
		gravityview()->log->debug( 'Fetching request with transient key {key}', array( 'key' => $key ) );
332
333 2
		$result = get_transient( $key );
334
335 2
		if ( is_wp_error( $result ) ) {
336
337
			gravityview()->log->debug( 'Fetching request resulted in error:', array( 'data' => $result ) );
338
339
			return false;
340
341 2
		} elseif ( $result ) {
342
343 2
			gravityview()->log->debug( 'Cached results found for  transient key {key}', array( 'key' => $key ) );
344
345 2
			return $result;
346
		}
347
348 2
		gravityview()->log->debug( 'No cached results found for  transient key {key}', array( 'key' => $key ) );
349
350 2
		return NULL;
351
352
	}
353
354
	/**
355
	 * Cache content as a transient.
356
	 *
357
	 * Cache time defaults to 1 week
358
	 *
359
	 * @param mixed $content     [description]
360
	 * @param string $filter_name Name used to modify the cache time. Will be set to `gravityview_cache_time_{$filter_name}`.
361
	 *
362
	 * @return bool If $content is not set, false. Otherwise, returns true if transient was set and false if not.
363
	 */
364 2
	public function set( $content, $filter_name = '' ) {
365
366
		// Don't cache empty results
367 2
		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 2
			$cache_time = (int) apply_filters( 'gravityview_cache_time_' . $filter_name, DAY_IN_SECONDS );
374
375 2
			gravityview()->log->debug( 'Setting cache with transient key {key} for {cache_time} seconds', array( 'key' => $this->key, 'cache_time' => $cache_time ) );
376
377 2
			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 void
396
	 */
397 2
	public function delete( $form_ids = NULL ) {
398 2
		global $wpdb;
399
400
		// Use object var if exists
401 2
		$form_ids = is_null( $form_ids ) ? $this->form_ids : $form_ids;
402
403 2
		if ( empty( $form_ids ) ) {
404
			gravityview()->log->debug( 'Did not delete cache; empty form IDs' );
405
406
			return;
407
		}
408
409 2
		foreach ( (array) $form_ids as $form_id ) {
410
411 2
			$key = '_transient_gv-cache-';
412
413
			$key = $wpdb->esc_like( $key );
414 2
415 2
			$form_id = intval( $form_id );
416
417
			// Find the transients containing this form
418
			$key = "$key%f:$form_id-%"; // \_transient\_gv-cache-%f:1-% for example
419
			$sql = $wpdb->prepare( "SELECT option_name FROM {$wpdb->options} WHERE `option_name` LIKE %s", $key );
420 2
421
			foreach ( ( $transients = $wpdb->get_col( $sql ) ) as $transient ) {
422
				// We have to delete it via the API to make sure the object cache is updated appropriately
423 2
				delete_transient( preg_replace( '#^_transient_#', '', $transient ) );
424 2
			}
425
426 2
			gravityview()->log->debug( 'Deleting cache for form #{form_id}', array( 'form_id' => $form_id, 'data' => array(
427
				$sql,
428 2
				sprintf( 'Deleted results: %d', count( $transients ) )
429
			) ) );
430
		}
431 2
432 2
	}
433 2
434
	/**
435
	 * Schedule expired transient cleanup twice a day.
436
	 *
437 2
	 * Can be overruled by the `gravityview_cleanup_transients` filter (returns boolean)
438
	 *
439
	 * @return void
440
	 */
441
	public function schedule_transient_cleanup() {
442
443
		/**
444
		 * @filter `gravityview_cleanup_transients` Override GravityView cleanup of transients by setting this to false
445
		 * @param boolean $cleanup Whether to run the GravityView auto-cleanup of transients. Default: `true`
446 2
		 */
447
		$cleanup = apply_filters( 'gravityview_cleanup_transients', true );
448
449
		if ( ! $cleanup ) {
450
			return;
451
		}
452 2
453
		if ( ! wp_next_scheduled( 'gravityview-expired-transients' ) ) {
454 2
			wp_schedule_event( time(), 'daily', 'gravityview-expired-transients' );
455
		}
456
	}
457
458 2
	/**
459 1
	 * Delete expired transients.
460
	 *
461 2
	 * The code is copied from the Delete Expired Transients, with slight modifications to track # of results and to get the blog ID dynamically
462
	 *
463
	 * @see https://wordpress.org/plugins/delete-expired-transients/ Plugin where the code was taken from
464
	 * @see  DelxtransCleaners::clearBlogExpired()
465
	 * @return void
466
	 */
467
	public function delete_expired_transients() {
468
		global $wpdb;
469
470
		// Added this line, which isn't in the plugin
471
		$blog_id = get_current_blog_id();
472
473
		$num_results = 0;
474
475
		// get current PHP time, offset by a minute to avoid clashes with other tasks
476
		$threshold = time() - 60;
477
478
		// get table name for options on specified blog
479
		$table = $wpdb->get_blog_prefix( $blog_id ) . 'options';
480
481
		// delete expired transients, using the paired timeout record to find them
482
		$sql = "
483
			delete from t1, t2
484
			using $table t1
485
			join $table t2 on t2.option_name = replace(t1.option_name, '_timeout', '')
486
			where (t1.option_name like '\_transient\_timeout\_%' or t1.option_name like '\_site\_transient\_timeout\_%')
487
			and t1.option_value < '$threshold'
488
		";
489
490
		$num_results = $wpdb->query( $sql );
491
492
		// delete orphaned transient expirations
493
		// also delete NextGEN Gallery 2.x display cache timeout aliases
494
		$sql = "
495
			delete from $table
496
			where (
497
				   option_name like '\_transient\_timeout\_%'
498
				or option_name like '\_site\_transient\_timeout\_%'
499
				or option_name like 'displayed\_galleries\_%'
500
				or option_name like 'displayed\_gallery\_rendering\_%'
501
			)
502
			and option_value < '$threshold'
503
		";
504
505
		$num_results += $wpdb->query( $sql );
506
507
		gravityview()->log->debug( 'Deleted {count} expired transient records from the database', array( 'count' => $num_results ) );
508
	}
509
510
	/**
511
	 * Check whether to use cached results, if available
512
	 *
513
	 * If the user can edit posts, they are able to override whether to cache results by adding `cache` or `nocache` to the URL requested.
514
	 *
515
	 * @return boolean True: use cache; False: don't use cache
516
	 */
517
	public function use_cache() {
518
519
		// Exit early if debugging (unless running PHPUnit)
520
		if ( defined( 'WP_DEBUG' ) && WP_DEBUG && ! ( defined('DOING_GRAVITYVIEW_TESTS' ) && DOING_GRAVITYVIEW_TESTS ) ) {
521
			return apply_filters( 'gravityview_use_cache', false, $this );
522 2
		}
523
524
		$use_cache = true;
525 2
526
		if ( GVCommon::has_cap( 'edit_gravityviews' ) ) {
527
528
			if ( isset( $_GET['cache'] ) || isset( $_GET['nocache'] ) ) {
529 2
530
				gravityview()->log->debug( 'Not using cache: ?cache or ?nocache is in the URL' );
531 2
532
				$use_cache = false;
533
			}
534
535
		}
536
537
		// Has the form been flagged as having changed items in it?
538
		if ( $this->in_blacklist() || ! $use_cache ) {
539
540
			// Delete caches for all items with form IDs XYZ
541
			$this->delete( $this->form_ids );
542
543 2
			// Remove the form from
544
			$this->blacklist_remove( $this->form_ids );
545
546
		}
547
548
		/**
549
		 * @filter `gravityview_use_cache` Modify whether to use the cache or not
550
		 * @param[out,in]  boolean $use_cache Previous setting
551
		 * @param[out] GravityView_Cache $this The GravityView_Cache object
552
		 */
553
		$use_cache = apply_filters( 'gravityview_use_cache', $use_cache, $this );
554
555
		return (boolean) $use_cache;
556
	}
557
558 2
}
559
560
new GravityView_Cache;
561