Completed
Push — develop ( 7592b8...3eb7c5 )
by Zack
14:44
created

includes/class-gravityview-entry-approval.php (1 issue)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
/**
3
 * @file class-gravityview-entry-approval.php
4
 * @package   GravityView
5
 * @license   GPL2+
6
 * @author    Katz Web Services, Inc.
7
 * @link      https://gravityview.co
8
 * @copyright Copyright 2016, Katz Web Services, Inc.
9
 *
10
 * @since 1.18
11
 */
12
13
/** If this file is called directly, abort. */
14
if ( ! defined( 'ABSPATH' ) ) {
15
	die;
16
}
17
18
/**
19
 * Generate linked list output for a list of entries.
20
 *
21
 * @since 1.18
22
 */
23
class GravityView_Entry_Approval {
24
25
	/**
26
	 * @var string Key used to store approval status in the Gravity Forms entry meta table
27
	 */
28
	const meta_key = 'is_approved';
29
30
	public function __construct() {
31
		$this->add_hooks();
32
	}
33
34
	/**
35
	 * Add actions and filters related to entry approval
36
	 *
37
	 * @return void
38
	 */
39
	private function add_hooks() {
40
41
		// in case entry is edited (on admin or frontend)
42
		add_action( 'gform_after_update_entry', array( $this, 'after_update_entry_update_approved_meta' ), 10, 2);
43
44
		// when using the User opt-in field, check on entry submission
45
		add_action( 'gform_after_submission', array( $this, 'after_submission' ), 10, 2 );
46
47
		// process ajax approve entry requests
48
		add_action('wp_ajax_gv_update_approved', array( $this, 'ajax_update_approved'));
49
50
	}
51
52
	/**
53
	 * Get the approval status for an entry
54
	 *
55
	 * @since 1.18
56
	 * @uses GVCommon::get_entry_id() Accepts entry slug or entry ID
57
	 *
58
	 * @param array|int|string $entry Entry array, entry slug, or entry ID
59
	 * @param string $value_or_label "value" or "label" (default: "label")
60
	 *
61
	 * @return bool|string Return the label or value of entry approval
62
	 */
63
	public static function get_entry_status( $entry, $value_or_label = 'label' ) {
64
65
		$entry_id = is_array( $entry ) ? $entry['id'] : GVCommon::get_entry_id( $entry, true );
66
67
		$status = gform_get_meta( $entry_id, self::meta_key );
68
69
		$status = GravityView_Entry_Approval_Status::maybe_convert_status( $status );
70
71
		if( 'value' === $value_or_label ) {
72
			return $status;
73
		}
74
75
		return GravityView_Entry_Approval_Status::get_label( $status );
76
	}
77
78
	/**
79
	 * Approve/Disapprove entries using the × or ✓ icons in the GF Entries screen
80
	 *
81
	 * @uses wp_send_json_error()
82
	 * @uses wp_send_json_success()
83
	 *
84
	 * Expects a $_POST request with the following $_POST keys and values:
85
	 *
86
	 * @global array $_POST {
87
	 * @type int $form_id ID of the form connected to the entry being updated
88
	 * @type string|int $entry_slug The ID or slug of the entry being updated
89
	 * @type string $approved The value of the entry approval status {@see GravityView_Entry_Approval_Status::is_valid() }
90
	 * }
91
	 *
92
	 * @return void Prints result using wp_send_json_success() and wp_send_json_error()
93
	 */
94
	public function ajax_update_approved() {
95
		
96
		$form_id = intval( rgpost('form_id') );
97
98
		// We always want requests from the admin to allow entry IDs, but not from the frontend
99
		// There's another nonce sent when approving entries in the admin that we check
100
		$force_entry_ids = rgpost( 'admin_nonce' ) && wp_verify_nonce( rgpost( 'admin_nonce' ), 'gravityview_admin_entry_approval' );
101
		
102
		$entry_id = GVCommon::get_entry_id( rgpost('entry_slug'), $force_entry_ids );
103
104
		$approval_status = rgpost('approved');
105
106
		$nonce = rgpost('nonce');
107
108
		// Valid status
109
		if( ! GravityView_Entry_Approval_Status::is_valid( $approval_status ) ) {
110
111
			do_action( 'gravityview_log_error', __METHOD__ . ': Invalid approval status', $_POST );
112
113
			$result = new WP_Error( 'invalid_status', __( 'The request was invalid. Refresh the page and try again.', 'gravityview' ) );
114
115
		}
116
117
		// Valid values
118
		elseif ( empty( $entry_id ) || empty( $form_id ) ) {
119
120
			do_action( 'gravityview_log_error', __METHOD__ . ' entry_id or form_id are empty.', $_POST );
121
122
			$result = new WP_Error( 'empty_details', __( 'The request was invalid. Refresh the page and try again.', 'gravityview' ) );
123
124
		}
125
126
		// Valid nonce
127
		else if ( empty( $nonce ) || ! wp_verify_nonce( $nonce, 'gravityview_entry_approval' ) ) {
128
129
			do_action( 'gravityview_log_error', __METHOD__ . ' Security check failed.', $_POST );
130
131
			$result = new WP_Error( 'invalid_nonce', __( 'The request was invalid. Refresh the page and try again.', 'gravityview' ) );
132
133
		}
134
135
		// Has capability
136
		elseif ( ! GVCommon::has_cap( 'gravityview_moderate_entries', $entry_id ) ) {
137
138
			do_action( 'gravityview_log_error', __METHOD__ . ' User does not have the `gravityview_moderate_entries` capability.' );
139
140
			$result = new WP_Error( 'Missing Cap: gravityview_moderate_entries', __( 'You do not have permission to edit this entry.', 'gravityview') );
141
142
		}
143
144
		// All checks passed
145
		else {
146
147
			$result = self::update_approved( $entry_id, $approval_status, $form_id );
148
149
		}
150
151
		if ( is_wp_error( $result ) ) {
152
			do_action( 'gravityview_log_error', __METHOD__ . ' Error updating approval: ' . $result->get_error_message() );
153
154
			wp_send_json_error( $result );
155
		}
156
157
		$current_status = self::get_entry_status( $entry_id, 'value' );
158
159
		wp_send_json_success( array(
160
			'status' => $current_status
161
		) );
162
	}
163
164
	/**
165
	 * Update the is_approved meta whenever the entry is submitted (and it contains a User Opt-in field)
166
	 *
167
	 * @since 1.16.6
168
	 *
169
	 * @param $entry array Gravity Forms entry object
170
	 * @param $form array Gravity Forms form object
171
	 */
172
	public function after_submission( $entry, $form ) {
173
		$this->after_update_entry_update_approved_meta( $form , $entry['id'] );
174
	}
175
176
	/**
177
	 * Update the is_approved meta whenever the entry is updated
178
	 *
179
	 * @since 1.7.6.1 Was previously named `update_approved_meta`
180
	 *
181
	 * @param  array $form     Gravity Forms form array
182
	 * @param  int $entry_id ID of the Gravity Forms entry
183
	 * @return void
184
	 */
185 3
	public function after_update_entry_update_approved_meta( $form, $entry_id = NULL ) {
186
187 3
		$approved_column = self::get_approved_column( $form['id'] );
188
189
		/**
190
		 * If the form doesn't contain the approve field, don't assume anything.
191
		 */
192 3
		if( empty( $approved_column ) ) {
193 3
			return;
194
		}
195
196
		$entry = GFAPI::get_entry( $entry_id );
197
198
		// If the checkbox is blank, it's disapproved, regardless of the label
199
		if ( '' === rgar( $entry, $approved_column ) ) {
200
			$value = GravityView_Entry_Approval_Status::DISAPPROVED;
201
		} else {
202
			// If the checkbox is not blank, it's approved
203
			$value = GravityView_Entry_Approval_Status::APPROVED;
204
		}
205
206
		self::update_approved_meta( $entry_id, $value, $form['id'] );
207
	}
208
209
	/**
210
	 * Process a bulk of entries to update the approve field/property
211
	 *
212
	 * @since 1.18 Moved to GravityView_Entry_Approval
213
	 * @since 1.18 Made public
214
	 *
215
	 * @access public
216
	 * @static
217
	 * @param array|boolean $entries If array, array of entry IDs that are to be updated. If true: update all entries.
218
	 * @param int $approved Approved status. If `0`: unapproved, if not empty, `Approved`
219
	 * @param int $form_id The Gravity Forms Form ID
220
	 * @return boolean|null True: successfully updated all entries. False: there was an error updating at least one entry. NULL: an error occurred (see log)
221
	 */
222 1
	public static function update_bulk( $entries = array(), $approved, $form_id ) {
223
224 1
		if( empty($entries) || ( $entries !== true && !is_array($entries) ) ) {
225
			do_action( 'gravityview_log_error', __METHOD__ . ' Entries were empty or malformed.', $entries );
226
			return NULL;
227
		}
228
229 1
		if( ! GVCommon::has_cap( 'gravityview_moderate_entries' ) ) {
230 1
			do_action( 'gravityview_log_error', __METHOD__ . ' User does not have the `gravityview_moderate_entries` capability.' );
231 1
			return NULL;
0 ignored issues
show
TRUE, FALSE and NULL must be lowercase; expected null, but found NULL.
Loading history...
232
		}
233
234
235 1
		if ( ! GravityView_Entry_Approval_Status::is_valid( $approved ) ) {
236
			do_action( 'gravityview_log_error', __METHOD__ . ' Invalid approval status', $approved );
237
			return NULL;
238
		}
239
240
		// calculate approved field id once instead of looping through in the update_approved() method
241 1
		$approved_column_id = self::get_approved_column( $form_id );
242
243 1
		$success = true;
244 1
		foreach( $entries as $entry_id ) {
245 1
			$update_success = self::update_approved( (int)$entry_id, $approved, $form_id, $approved_column_id );
246
247 1
			if( ! $update_success ) {
248 1
				$success = false;
249
			}
250
		}
251
252 1
		return $success;
253
	}
254
255
	/**
256
	 * update_approved function.
257
	 *
258
	 * @since 1.18 Moved to GravityView_Entry_Approval class
259
	 *
260
	 * @access public
261
	 * @static
262
	 * @param int $entry_id (default: 0)
263
	 * @param int $approved (default: 2)
264
	 * @param int $form_id (default: 0)
265
	 * @param int $approvedcolumn (default: 0)
266
	 *
267
	 * @return boolean True: It worked; False: it failed
268
	 */
269 1
	public static function update_approved( $entry_id = 0, $approved = 2, $form_id = 0, $approvedcolumn = 0 ) {
270
271 1
		if( !class_exists( 'GFAPI' ) ) {
272
			do_action( 'gravityview_log_error', __METHOD__ . 'GFAPI does not exist' );
273
			return false;
274
		}
275
276 1
		if( ! GravityView_Entry_Approval_Status::is_valid( $approved ) ) {
277
			do_action( 'gravityview_log_error', __METHOD__ . ': Not a valid approval value.' );
278
			return false;
279
		}
280
281 1
		$approved = GravityView_Entry_Approval_Status::maybe_convert_status( $approved );
282
283 1
		$entry = GFAPI::get_entry( $entry_id );
284
285 1
		if ( is_wp_error( $entry ) ) {
286 1
			do_action( 'gravityview_log_error', __METHOD__ . ': Entry does not exist' );
287 1
			return false;
288
		}
289
290
		// If the form has an Approve/Reject field, update that value
291 1
		$result = self::update_approved_column( $entry_id, $approved, $form_id, $approvedcolumn );
292
293 1
		if( is_wp_error( $result ) ) {
294
			do_action( 'gravityview_log_error', __METHOD__ . sprintf( ' - Entry approval not updated: %s', $result->get_error_message() ) );
295
			return false;
296
		}
297
298 1
		$form_id = intval( $form_id );
299
300
		// Update the entry meta
301 1
		self::update_approved_meta( $entry_id, $approved, $form_id );
302
303
		// add note to entry if approval field updating worked or there was no approved field
304
		// There's no validation for the meta
305 1
		if( true === $result ) {
306
307
			// Add an entry note
308 1
			self::add_approval_status_updated_note( $entry_id, $approved );
309
310
			/**
311
			 * Destroy the cache for this form
312
			 * @see class-cache.php
313
			 * @since 1.5.1
314
			 */
315 1
			do_action( 'gravityview_clear_form_cache', $form_id );
316
317
		}
318
319 1
		return $result;
320
	}
321
322
	/**
323
	 * Add a note when an entry is approved
324
	 *
325
	 * @see GravityView_Entry_Approval::update_approved
326
	 *
327
	 * @since 1.18
328
	 *
329
	 * @param int $entry_id Gravity Forms entry ID
330
	 * @param int $approved Approval status
331
	 *
332
	 * @return false|int|WP_Error Note ID if successful; WP_Error if error when adding note, FALSE if note not updated because of `gravityview/approve_entries/add-note` filter or `GravityView_Entry_Notes` class not existing
333
	 */
334 1
	private static function add_approval_status_updated_note( $entry_id, $approved = 0 ) {
335 1
		$note = '';
336
337
		switch ( $approved ) {
338 1
			case GravityView_Entry_Approval_Status::APPROVED:
339 1
				$note = __( 'Approved the Entry for GravityView', 'gravityview' );
340 1
				break;
341
			case GravityView_Entry_Approval_Status::UNAPPROVED:
342
				$note = __( 'Reset Entry approval for GravityView', 'gravityview' );
343
				break;
344
			case GravityView_Entry_Approval_Status::DISAPPROVED:
345
				$note = __( 'Disapproved the Entry for GravityView', 'gravityview' );
346
				break;
347
		}
348
349
		/**
350
		 * @filter `gravityview/approve_entries/add-note` Add a note when the entry has been approved or disapproved?
351
		 * @since 1.16.3
352
		 * @param bool $add_note True: Yep, add that note! False: Do not, under any circumstances, add that note!
353
		 */
354 1
		$add_note = apply_filters( 'gravityview/approve_entries/add-note', true );
355
356 1
		$note_id = false;
357
358 1
		if( $add_note && class_exists( 'GravityView_Entry_Notes' ) ) {
359
360 1
			$current_user = wp_get_current_user();
361
362 1
			$note_id = GravityView_Entry_Notes::add_note( $entry_id, $current_user->ID, $current_user->display_name, $note );
363
		}
364
365 1
		return $note_id;
366
	}
367
368
	/**
369
	 * Update the Approve/Disapproved field value
370
	 *
371
	 * @param  int $entry_id ID of the Gravity Forms entry
372
	 * @param  string $status String whether entry is approved or not. `0` for not approved, `Approved` for approved.
373
	 * @param int $form_id ID of the form of the entry being updated. Improves query performance.
374
	 * @param string $approvedcolumn Gravity Forms Field ID
375
	 *
376
	 * @return true|WP_Error
377
	 */
378
	private static function update_approved_column( $entry_id = 0, $status = '0', $form_id = 0, $approvedcolumn = 0 ) {
379
380
		if( empty( $approvedcolumn ) ) {
381
			$approvedcolumn = self::get_approved_column( $form_id );
382
		}
383
384
		if ( empty( $approvedcolumn ) ) {
385
			return true;
386
		}
387
388
		if ( ! GravityView_Entry_Approval_Status::is_valid( $status ) ) {
389
			return new WP_Error( 'invalid_status', 'Invalid entry approval status', $status );
390
		}
391
392
		//get the entry
393
		$entry = GFAPI::get_entry( $entry_id );
394
395
		// Entry doesn't exist
396
		if ( is_wp_error( $entry ) ) {
397
			return $entry;
398
		}
399
400
		$status = GravityView_Entry_Approval_Status::maybe_convert_status( $status );
401
402
		$new_value = '';
403
		if( GravityView_Entry_Approval_Status::APPROVED === $status ) {
404
			$new_value = self::get_approved_column_input_label( $form_id, $approvedcolumn );
405
		}
406
407
		//update entry
408
		$entry["{$approvedcolumn}"] = $new_value;
409
410
		/**
411
		 * Note: GFAPI::update_entry() doesn't trigger `gform_after_update_entry`, so we trigger updating the meta ourselves
412
		 * @see GravityView_Entry_Approval::after_update_entry_update_approved_meta
413
		 * @var true|WP_Error $result
414
		 */
415
		$result = GFAPI::update_entry( $entry );
416
		
417
		return $result;
418
	}
419
420
	/**
421
	 * Get the value for the approved field checkbox
422
	 *
423
	 * When approving a field via the entry meta, use the correct value for the new approved column input
424
	 *
425
	 * @since 1.19
426
	 *
427
	 * @param array|int $form Form ID or form array
428
	 * @param string $approved_column Approved column field ID
429
	 *
430
	 * @return string|null
431
	 */
432
	private static function get_approved_column_input_label( $form, $approved_column ) {
433
434
		$field = gravityview_get_field( $form, $approved_column );
435
436
		// If the user has enabled a different value than the label (for some reason), use it.
437
		// This is highly unlikely
438
		if ( is_array( $field->choices ) && ! empty( $field->choices ) ) {
439
			return isset( $field->choices[0]['value'] ) ? $field->choices[0]['value'] : $field->choices[0]['text'];
440
		}
441
442
		// Otherwise, fall back on the inputs array
443
		if ( is_array( $field->inputs ) && ! empty( $field->inputs ) ) {
444
			return $field->inputs[0]['label'];
445
		}
446
447
		return null;
448
	}
449
450
	/**
451
	 * Update the `is_approved` entry meta value
452
	 *
453
	 * @since 1.7.6.1 `after_update_entry_update_approved_meta` was previously to be named `update_approved_meta`
454
	 * @since 1.17.1 Added $form_id parameter
455
	 *
456
	 * @param  int $entry_id ID of the Gravity Forms entry
457
	 * @param  string $status String whether entry is approved or not. `0` for not approved, `Approved` for approved.
458
	 * @param int $form_id ID of the form of the entry being updated. Improves query performance.
459
	 *
460
	 * @return void
461
	 */
462
	private static function update_approved_meta( $entry_id, $status, $form_id = 0 ) {
463
464
		if ( ! GravityView_Entry_Approval_Status::is_valid( $status ) ) {
465
			do_action('gravityview_log_error', __METHOD__ . ': $is_approved not valid value', $status );
466
			return;
467
		}
468
469
		$status = GravityView_Entry_Approval_Status::maybe_convert_status( $status );
470
471
		// update entry meta
472
		if( function_exists('gform_update_meta') ) {
473
474
			if( GravityView_Entry_Approval_Status::is_unapproved( $status ) ) {
475
				gform_delete_meta( $entry_id, self::meta_key );
476
			} else {
477
				gform_update_meta( $entry_id, self::meta_key, $status, $form_id );
478
			}
479
480
			/**
481
			 * @action `gravityview/approve_entries/updated` Triggered when an entry approval is updated
482
			 * @since 1.7.6.1
483
			 * @param  int $entry_id ID of the Gravity Forms entry
484
			 * @param  string|int $status String whether entry is approved or not. See GravityView_Entry_Approval_Status for valid statuses.
485
			 */
486
			do_action( 'gravityview/approve_entries/updated', $entry_id, $status );
487
488
			$action = GravityView_Entry_Approval_Status::get_key( $status );
489
490
			/**
491
			 * @action `gravityview/approve_entries/{$action}` Triggered when an entry approval is reset.
492
			 * $action can be 'approved', 'unapproved', or 'disapproved'
493
			 * @since 1.7.6.1
494
			 * @since 1.18 Added "unapproved"
495
			 * @param  int $entry_id ID of the Gravity Forms entry
496
			 */
497
			do_action( 'gravityview/approve_entries/' . $action , $entry_id );
498
499
		} else {
500
501
			do_action('gravityview_log_error', __METHOD__ . ' - `gform_update_meta` does not exist.' );
502
503
		}
504
	}
505
506
	/**
507
	 * Calculate the approve field.input id
508
	 *
509
	 * @access public
510
	 * @static
511
	 * @param mixed $form GF Form or Form ID
512
	 * @return false|null|string Returns the input ID of the approved field. Returns NULL if no approved fields were found. Returns false if $form_id wasn't set.
513
	 */
514 3
	static public function get_approved_column( $form ) {
515
516 3
		if( empty( $form ) ) {
517
			return null;
518
		}
519
520 3
		if( !is_array( $form ) ) {
521 3
			$form = GVCommon::get_form( $form );
522
		}
523
524 3
		$approved_column_id = null;
525
526
		/**
527
		 * @var string $key
528
		 * @var GF_Field $field
529
		 */
530 3
		foreach( $form['fields'] as $key => $field ) {
531
532 3
			$inputs = $field->get_entry_inputs();
533
534 3
			if( !empty( $field->gravityview_approved ) ) {
535
				if ( ! empty( $inputs ) && !empty( $inputs[0]['id'] ) ) {
536
					$approved_column_id = $inputs[0]['id'];
537
					break;
538
				}
539
			}
540
541
			// Note: This is just for backward compatibility from GF Directory plugin and old GV versions - when using i18n it may not work..
542 3
			if( 'checkbox' === $field->type && ! empty( $inputs ) ) {
543
				foreach ( $inputs as $input ) {
544
					if ( 'approved' === strtolower( $input['label'] ) ) {
545
						$approved_column_id = $input['id'];
546 3
						break;
547
					}
548
				}
549
			}
550
		}
551
552 3
		return $approved_column_id;
553
	}
554
555
}
556
557
new GravityView_Entry_Approval;