Completed
Push — develop ( 41ba6f...08d5d4 )
by Zack
29:01 queued 09:01
created

GravityView_Duplicate_Entry::maybe_not_visible()   B

Complexity

Conditions 6
Paths 6

Size

Total Lines 28

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 6

Importance

Changes 0
Metric Value
cc 6
nc 6
nop 3
dl 0
loc 28
ccs 4
cts 4
cp 1
crap 6
rs 8.8497
c 0
b 0
f 0
1
<?php
2
/**
3
 * The GravityView Duplicate Entry Extension
4
 *
5
 * Duplicate entries in GravityView.
6
 *
7
 * @since     2.5
8
 * @package   GravityView
9
 * @license   GPL2+
10
 * @author    GravityView <[email protected]>
11
 * @link      http://gravityview.co
12
 * @copyright Copyright 2014, Katz Web Services, Inc.
13
 */
14
15
if ( ! defined( 'WPINC' ) ) {
16
	die;
17
}
18
19
/**
20
 * @since 2.5
21
 */
22
final class GravityView_Duplicate_Entry {
23
24
	/**
25
	 * @var string The location of this file.
26
	 */
27
	static $file;
28
29
	/**
30
	 * @var GravityView_Duplicate_Entry This instance.
31
	 */
32
	static $instance;
33
34
	var $view_id;
35
36 1
	function __construct() {
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...
37
38 1
		self::$file = plugin_dir_path( __FILE__ );
39 1
		$this->add_hooks();
40 1
	}
41
42
	/**
43
	 * @since 2.5
44
	 */
45 1
	private function add_hooks() {
46
47 1
		add_action( 'wp', array( $this, 'process_duplicate' ), 10000 );
48
49 1
		add_filter( 'gravityview_entry_default_fields', array( $this, 'add_default_field' ), 10, 3 );
50
51 1
		add_action( 'gravityview_before', array( $this, 'maybe_display_message' ) );
52
53
		// For the Duplicate Entry Link, you don't want visible to all users.
54 1
		add_filter( 'gravityview_field_visibility_caps', array( $this, 'modify_visibility_caps' ), 10, 5 );
55
56
		// Modify the field options based on the name of the field type
57 1
		add_filter( 'gravityview_template_duplicate_link_options', array( $this, 'duplicate_link_field_options' ), 10, 5 );
58
59
		// add template path to check for field
60 1
		add_filter( 'gravityview_template_paths', array( $this, 'add_template_path' ) );
61
62
		// Entry duplication in the backend
63 1
		add_action( 'gform_entries_first_column_actions', array( $this, 'make_duplicate_link_row' ), 10, 5 );
64
65
		// Handle duplicate action in the backend
66 1
		add_action( 'gform_pre_entry_list', array( $this, 'maybe_duplicate_list' ) );
67
68 1
		add_filter( 'gravityview/sortable/field_blacklist', array( $this, '_filter_sortable_fields' ), 1 );
69 1
70
		add_filter( 'gravityview/field/is_visible', array( $this, 'maybe_not_visible' ), 10, 3 );
71
	}
72
73
	/**
74
	 * Return the instantiated class object
75
	 *
76
	 * @since  2.5
77 2
	 * @return GravityView_Duplicate_Entry
78
	 */
79 2
	static public function getInstance() {
80 1
81
		if ( empty( self::$instance ) ) {
82
			self::$instance = new self;
83 2
		}
84
85
		return self::$instance;
86
	}
87
88
	/**
89
	 * Hide the field or not.
90
	 *
91
	 * For non-logged in users.
92
	 * For users that have no duplicate rights on any of the current entries.
93
	 *
94
	 * @param bool $visible Visible or not.
95 1
	 * @param \GV\Field $field The field.
96
	 * @param \GV\View $view The View context.
97 1
	 *
98
	 * @return bool
99 1
	 */
100
	public function maybe_not_visible( $visible, $field, $view ) {
101 1
102
		if ( 'duplicate_link' !== $field->ID ) {
103
			return $visible;
104
		}
105
106
		if ( ! $view ) {
107
			return $visible;
108
		}
109
110
		static $visibility_cache_for_view = array();
111
112
		if ( ! is_null( $result = \GV\Utils::get( $visibility_cache_for_view, $view->ID, null ) ) ) {
113 2
			return $result;
114
		}
115
116
		foreach ( $view->get_entries()->all() as $entry ) {
117 2
			if ( self::check_user_cap_duplicate_entry( $entry->as_entry(), $view ) ) {
118
				// At least one entry is duplicable for this user
119 2
				$visibility_cache_for_view[ $view->ID ] = true;
120
				return true;
121
			}
122
		}
123
124
		$visibility_cache_for_view[ $view->ID ] = false;
125
126
		return false;
127
	}
128
129
	/**
130
	 * Prevent users from being able to sort by the Duplicate field
131
	 *
132
	 * @since 2.8.3
133
	 *
134
	 * @param array $fields Array of field types not editable by users
135
	 *
136
	 * @return array
137
	 */
138
	public function _filter_sortable_fields( $fields ) {
139
140
		$fields = (array) $fields;
141
142
		$fields[] = 'duplicate_link';
143
144
		return $fields;
145
	}
146
147
	/**
148
	 * Include this extension templates path
149
	 *
150
	 * @since  2.5
151
	 *
152
	 * @param array $file_paths List of template paths ordered
153
	 *
154
	 * @return array File paths, with duplicate field path added at index 117
155
	 */
156
	public function add_template_path( $file_paths ) {
157
158
		// Index 100 is the default GravityView template path.
159
		// Index 110 is Edit Entry link
160
		$file_paths[ 117 ] = self::$file;
161
162
		return $file_paths;
163
	}
164
165
	/**
166
	 * Add "Duplicate Link Text" setting to the edit_link field settings
167
	 *
168
	 * @since  2.5
169
	 *
170
	 * @param  array  $field_options [description]
171
	 * @param  [type] $template_id   [description]
0 ignored issues
show
Documentation introduced by
The doc-type [type] could not be parsed: Unknown type name "" at position 0. [(view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
172
	 * @param  [type] $field_id      [description]
0 ignored issues
show
Documentation introduced by
The doc-type [type] could not be parsed: Unknown type name "" at position 0. [(view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
173
	 * @param  [type] $context       [description]
0 ignored issues
show
Documentation introduced by
The doc-type [type] could not be parsed: Unknown type name "" at position 0. [(view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
174
	 * @param  [type] $input_type    [description]
0 ignored issues
show
Documentation introduced by
The doc-type [type] could not be parsed: Unknown type name "" at position 0. [(view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
175
	 *
176
	 * @return array                [description]
177
	 */
178
	public function duplicate_link_field_options( $field_options, $template_id, $field_id, $context, $input_type ) {
179
180
		// Always a link, never a filter, always same window
181
		unset( $field_options['show_as_link'], $field_options['search_filter'], $field_options['new_window'] );
182
183
		// Duplicate Entry link should only appear to visitors capable of editing entries
184
		unset( $field_options['only_loggedin'], $field_options['only_loggedin_cap'] );
185
186
		$add_option['duplicate_link'] = array(
0 ignored issues
show
Coding Style Comprehensibility introduced by
$add_option was never initialized. Although not strictly required by PHP, it is generally a good practice to add $add_option = array(); before regardless.

Adding an explicit array definition is generally preferable to implicit array definition as it guarantees a stable state of the code.

Let’s take a look at an example:

foreach ($collection as $item) {
    $myArray['foo'] = $item->getFoo();

    if ($item->hasBar()) {
        $myArray['bar'] = $item->getBar();
    }

    // do something with $myArray
}

As you can see in this example, the array $myArray is initialized the first time when the foreach loop is entered. You can also see that the value of the bar key is only written conditionally; thus, its value might result from a previous iteration.

This might or might not be intended. To make your intention clear, your code more readible and to avoid accidental bugs, we recommend to add an explicit initialization $myArray = array() either outside or inside the foreach loop.

Loading history...
187
			'type' => 'text',
188
			'label' => __( 'Duplicate Link Text', 'gravityview' ),
189
			'desc' => NULL,
190
			'value' => __( 'Duplicate Entry', 'gravityview' ),
191
			'merge_tags' => true,
192
		);
193
194
		$field_options['allow_duplicate_cap'] = array(
195
			'type' => 'select',
196
			'label' => __( 'Allow the following users to duplicate the entry:', 'gravityview' ),
197
			'choices' => GravityView_Render_Settings::get_cap_choices( $template_id, $field_id, $context, $input_type ),
198
			'tooltip' => 'allow_duplicate_cap',
199
			'class' => 'widefat',
200
			'value' => 'read', // Default: entry creator
201
		);
202
203
		return array_merge( $add_option, $field_options );
204
	}
205
206
207
	/**
208
	 * Add Edit Link as a default field, outside those set in the Gravity Form form
209
	 *
210
	 * @since 2.5
211
	 *
212
	 * @param array $entry_default_fields Existing fields
213
	 * @param  string|array $form form_ID or form object
214
	 * @param  string $zone   Either 'single', 'directory', 'edit', 'header', 'footer'
215
	 *
216
	 * @return array $entry_default_fields, with `duplicate_link` added. Won't be added if in Edit Entry context.
217
	 */
218
	public function add_default_field( $entry_default_fields, $form = array(), $zone = '' ) {
219
220
		if ( 'edit' !== $zone ) {
221
			$entry_default_fields['duplicate_link'] = array(
222
				'label' => __( 'Duplicate Entry', 'gravityview' ),
223
				'type'  => 'duplicate_link',
224
				'desc'  => __( 'A link to duplicate the entry. Respects the Duplicate Entry permissions.', 'gravityview' ),
225
				'icon'  => 'dashicons-controls-repeat',
226
			);
227
		}
228
229
		return $entry_default_fields;
230
	}
231
232
	/**
233
	 * Add Duplicate Entry Link to the Add Field dialog
234
	 *
235
	 * @since 2.5
236
	 *
237
	 * @param array $available_fields
238
	 *
239
	 * @return array Fields with `duplicate_link` added
240
	 */
241
	public function add_available_field( $available_fields = array() ) {
242
243
		$available_fields['duplicate_link'] = array(
244
			'label_text' => __( 'Duplicate Entry', 'gravityview' ),
245
			'field_id' => 'duplicate_link',
246
			'label_type' => 'field',
247
			'input_type' => 'duplicate_link',
248
			'field_options' => NULL
249 2
		);
250 2
251
		return $available_fields;
252
	}
253
254
	/**
255
	 * Change wording for the Edit context to read Entry Creator
256
	 *
257
	 * @since 2.5
258
	 *
259
	 * @param  array 	    $visibility_caps        Array of capabilities to display in field dropdown.
260
	 * @param  string       $field_type  Type of field options to render (`field` or `widget`)
0 ignored issues
show
Bug introduced by
There is no parameter named $field_type. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
261
	 * @param  string       $template_id Table slug
262
	 * @param  float|string $field_id    GF Field ID - Example: `3`, `5.2`, `entry_link`, `created_by`
263
	 * @param  string       $context     What context are we in? Example: `single` or `directory`
264
	 * @param  string       $input_type  (textarea, list, select, etc.)
265
	 *
266
	 * @return array                   Array of field options with `label`, `value`, `type`, `default` keys
267 2
	 */
268
	public function modify_visibility_caps( $visibility_caps = array(), $template_id = '', $field_id = '', $context = '', $input_type = '' ) {
0 ignored issues
show
Unused Code introduced by
The parameter $context is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $input_type is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
269 2
270
		$caps = $visibility_caps;
271 2
272
		// If we're configuring fields in the edit context, we want a limited selection
273
		if ( 'duplicate_link' === $field_id ) {
274
275
			// Remove other built-in caps.
276 2
			unset( $caps['publish_posts'], $caps['gravityforms_view_entries'], $caps['duplicate_others_posts'] );
277 2
278 2
			$caps['read'] = _x( 'Entry Creator', 'User capability', 'gravityview' );
279 2
		}
280 2
281 2
		return $caps;
282
	}
283 2
284
	/**
285
	 * Generate a consistent nonce key based on the Entry ID
286
	 *
287
	 * @since 2.5
288
	 *
289
	 * @param  int $entry_id Entry ID
290
	 *
291
	 * @return string           Key used to validate request
292
	 */
293
	public static function get_nonce_key( $entry_id ) {
294
		return sprintf( 'duplicate_%s', $entry_id );
295
	}
296
297
298
	/**
299
	 * Generate a nonce link with the base URL of the current View embed
300
	 *
301 3
	 * We don't want to link to the single entry, because when duplicated, there would be nothing to return to.
302
	 *
303
	 * @since 2.5
304 3
	 *
305 3
	 * @param  array       $entry Gravity Forms entry array
306
	 * @param  int         $view_id The View id. Not optional since 2.0
307
	 * @param  int         $post_id ID of the current post/page being embedded on, if any
308
	 *
309 1
	 * @return string|null If directory link is valid, the URL to process the duplicate request. Otherwise, `NULL`.
310
	 */
311 1
	public static function get_duplicate_link( $entry, $view_id, $post_id = null ) {
312 1
313 1
        $base = GravityView_API::directory_link( $post_id ? : $view_id, true );
314
315
		if ( empty( $base ) ) {
316
			gravityview()->log->error( 'Post ID does not exist: {post_id}', array( 'post_id' => $post_id ) );
317 1
			return NULL;
318
		}
319
320 1
		$actionurl = add_query_arg( array(
321
			'action'	=> 'duplicate',
322 1
			'entry_id'	=> $entry['id'],
323
			'gvid' => $view_id,
324 1
            'view_id' => $view_id,
325
		), $base );
326 1
327
		return add_query_arg( 'duplicate', wp_create_nonce( self::get_nonce_key( $entry['id'] ) ), $actionurl );
328
	}
329 1
330 1
	/**
331
	 * Handle the duplication request, if $_GET['action'] is set to "duplicate"
332
	 *
333
	 * 1. Check referrer validity
334
	 * 2. Make sure there's an entry with the slug of $_GET['entry_id']
335
	 * 3. If so, attempt to duplicate the entry. If not, set the error status
336 1
	 * 4. Remove `action=duplicate` from the URL
337
	 * 5. Redirect to the page using `wp_safe_redirect()`
338 1
	 *
339
	 * @since 2.5
340
	 *
341 1
	 * @uses wp_safe_redirect()
342 1
	 *
343
	 * @return void|string $url URL during tests instead of redirect.
344
	 */
345 1
	public function process_duplicate() {
346 1
347 1
		// If the form is submitted
348 1
		if ( ( ! isset( $_GET['action'] ) ) || 'duplicate' !== $_GET['action'] || ( ! isset( $_GET['entry_id'] ) ) ) {
349
			return;
350
		}
351
352
		// Make sure it's a GravityView request
353
		$valid_nonce_key = wp_verify_nonce( \GV\Utils::_GET( 'duplicate' ), self::get_nonce_key( $_GET['entry_id'] ) );
354 1
355
		if ( ! $valid_nonce_key ) {
356
			gravityview()->log->debug( 'Duplicate entry not processed: nonce validation failed.' );
357
			return;
358
		}
359
360
		// Get the entry slug
361
		$entry_slug = esc_attr( $_GET['entry_id'] );
362
363 1
		// See if there's an entry there
364
		$entry = gravityview_get_entry( $entry_slug, true, false );
365
366 1
		if ( $entry ) {
367 1
368
			$has_permission = $this->user_can_duplicate_entry( $entry );
369
370
			if ( is_wp_error( $has_permission ) ) {
371 1
372 1
				$messages = array(
373
					'message' => urlencode( $has_permission->get_error_message() ),
374 1
					'status' => 'error',
375 1
				);
376
377
			} else {
378
379
				// Duplicate the entry
380
				$duplicate_response = $this->duplicate_entry( $entry );
381
382
				if ( is_wp_error( $duplicate_response ) ) {
383
384
					$messages = array(
385
						'message' => urlencode( $duplicate_response->get_error_message() ),
386
						'status' => 'error',
387
					);
388
389
					gravityview()->log->error( 'Entry {entry_slug} cannot be duplicated: {error_code} {error_message}', array(
390
						'entry_slug'    => $entry_slug,
391
						'error_code'    => $duplicate_response->get_error_code(),
392
						'error_message' => $duplicate_response->get_error_message(),
393
					) );
394 1
395
				} else {
396 1
397
					$messages = array(
398
						'status' => $duplicate_response,
399
					);
400 1
401
				}
402 1
403
			}
404 1
405 1
		} else {
406
407 1
			gravityview()->log->error( 'Duplicate entry failed: there was no entry with the entry slug {entry_slug}', array( 'entry_slug' => $entry_slug ) );
408
409
			$messages = array(
410
				'message' => urlencode( __( 'The entry does not exist.', 'gravityview' ) ),
411 1
				'status' => 'error',
412
			);
413 1
		}
414 1
415 1
		$redirect_to_base = esc_url_raw( remove_query_arg( array( 'action', 'gvid', 'entry_id' ) ) );
416 1
		$redirect_to = add_query_arg( $messages, $redirect_to_base );
417 1
418 1
		if ( defined( 'DOING_GRAVITYVIEW_TESTS' ) ) {
419 1
			return $redirect_to;
420 1
		}
421 1
422
		wp_safe_redirect( $redirect_to );
423
424
		exit();
425
	}
426
427
	/**
428
	 * Duplicate the entry.
429 1
	 *
430
	 * Done after all the checks in self::process_duplicate.
431 1
	 *
432 1
	 * @since 2.5
433
	 *
434
	 * @param array $entry The entry to be duplicated
435 1
	 *
436
	 * @return WP_Error|boolean
437 1
	 */
438
	private function duplicate_entry( $entry ) {
439 1
440
		if ( ! $entry_id = \GV\Utils::get( $entry, 'id' ) ) {
441
			return new WP_Error( 'gravityview-duplicate-entry-missing', __( 'The entry does not exist.', 'gravityview' ) );
442 1
		}
443 1
444 1
		gravityview()->log->debug( 'Starting duplicate entry: {entry_id}', array( 'entry_id' => $entry_id ) );
445
446
		global $wpdb;
447 1
448 1
		$entry_table = GFFormsModel::get_entry_table_name();
449 1
		$entry_meta_table = GFFormsModel::get_entry_meta_table_name();
450 1
451 1
		if ( ! $row = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM $entry_table WHERE ID = %d", $entry_id ), ARRAY_A ) ) {
452 1
			return new WP_Error( 'gravityview-duplicate-entry-missing', __( 'The entry does not exist.', 'gravityview' ) );
453
		}
454
455
		$form = GFAPI::get_form( $entry['form_id'] );
456
457 1
		$row['id'] = null;
458
		$row['date_created'] = date( 'Y-m-d H:i:s', time() );
459
		$row['date_updated'] = $row['date_created'];
460
		$row['is_starred'] = false;
461
		$row['is_read'] = false;
462
		$row['ip'] = rgars( $form, 'personalData/preventIP' ) ? '' : GFFormsModel::get_ip();
463
		$row['source_url'] = esc_url_raw( remove_query_arg( array( 'action', 'gvid', 'result', 'duplicate', 'entry_id' ) ) );
464
		$row['user_agent'] = \GV\Utils::_SERVER( 'HTTP_USER_AGENT' );
465 1
		$row['created_by'] = wp_get_current_user()->ID;
466
467 1
		/**
468 1
		 * @filter `gravityview/entry/duplicate/details` Modify the new entry details before it's created.
469 1
		 * @since 2.5
470
		 * @param[in,out] array $row The entry details
471 1
		 * @param array $entry The original entry
472
		 */
473
		$row = apply_filters( 'gravityview/entry/duplicate/details', $row, $entry );
474
475
		if ( ! $wpdb->insert( $entry_table, $row ) ) {
476 1
			return new WP_Error( 'gravityview-duplicate-entry-db-details', __( 'There was an error duplicating the entry.', 'gravityview' ) );
477
		}
478 1
479
		$duplicated_id = $wpdb->insert_id;
480
481
		$meta = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM $entry_meta_table WHERE entry_id = %d", $entry_id ), ARRAY_A );
482
483
		$duplicate_meta = new WP_List_Util( $meta );
484
485
		// Keys that should be reset by default
486 1
		$reset_meta = array( 'is_approved', 'gravityview_unique_id', 'workflow_current_status_timestamp' );
487
		foreach ( $reset_meta as $meta_key ) {
488 1
			$duplicate_meta->filter( array( 'meta_key' => $meta_key ), 'NOT' );
489
		}
490 1
491
		$save_this_meta = array();
492
		foreach ( $duplicate_meta->get_output() as $m ) {
493
			$save_this_meta[] = array(
494
				'meta_key' => $m['meta_key'],
495
				'meta_value' => $m['meta_value'],
496
				'item_index' => $m['item_index'],
497
			);
498
		}
499
500 1
		// Update the row ID for later usage
501
		$row['id'] = $duplicated_id;
502
503 1
		/**
504
		 * @filter `gravityview/entry/duplicate/meta` Modify the new entry meta details.
505
		 * @param[in,out] array $save_this_meta The duplicate meta. Use/add meta_key, meta_value, item_index.
506
		 * @param array $row The duplicated entry
507 1
		 * @param array $entry The original entry
508
		 */
509 1
		$save_this_meta = apply_filters( 'gravityview/entry/duplicate/meta', $save_this_meta, $row, $entry );
510
511
		foreach ( $save_this_meta as $data ) {
512
			$data['form_id'] = $entry['form_id'];
513
			$data['entry_id'] = $duplicated_id;
514
515
			if ( ! $wpdb->insert( $entry_meta_table, $data ) ) {
516
				return new WP_Error( 'gravityview-duplicate-entry-db-meta', __( 'There was an error duplicating the entry.', 'gravityview' ) );
517
			}
518 1
		}
519
520 1
		$duplicated_entry = \GFAPI::get_entry( $duplicated_id );
521
522
		$duplicate_response = 'duplicated';
523
524
		/**
525
		 * @action `gravityview/duplicate-entry/duplicated` Triggered when an entry is duplicated
526
		 * @since 2.5
527
		 * @param  array $duplicated_entry The duplicated entry
528
		 * @param  array $entry The original entry
529
		*/
530 1
		do_action( 'gravityview/duplicate-entry/duplicated', $duplicated_entry, $entry );
531
532 1
		gravityview()->log->debug( 'Duplicate response: {duplicate_response}', array( 'duplicate_response' => $duplicate_response ) );
533
534
		return $duplicate_response;
535
	}
536
537
	/**
538
	 * Is the current nonce valid for editing the entry?
539 1
	 *
540
	 * @since 2.5
541 1
	 *
542 1
	 * @return boolean
543
	 */
544
	public function verify_nonce() {
545 1
546
		// No duplicate entry request was made
547
		if ( empty( $_GET['entry_id'] ) || empty( $_GET['duplicate'] ) ) {
548
			return false;
549
		}
550
551
		$nonce_key = self::get_nonce_key( $_GET['entry_id'] );
552
553
		$valid = wp_verify_nonce( $_GET['duplicate'], $nonce_key );
554
555
		/**
556
		 * @filter `gravityview/duplicate-entry/verify_nonce` Override Duplicate Entry nonce validation. Return true to declare nonce valid.
557
		 * @since 2.5
558
		 * @see wp_verify_nonce()
559
		 * @param int|boolean $valid False if invalid; 1 or 2 when nonce was generated
560
		 * @param string $nonce_key Name of nonce action used in wp_verify_nonce. $_GET['duplicate'] holds the nonce value itself. Default: `duplicate_{entry_id}`
561
		 */
562 1
		$valid = apply_filters( 'gravityview/duplicate-entry/verify_nonce', $valid, $nonce_key );
563
564 1
		return $valid;
565
	}
566 1
567
	/**
568
	 * Get the onclick attribute for the confirm dialogs that warns users before they duplicate an entry
569
	 *
570 1
	 * @since 2.5
571 1
	 *
572
	 * @return string HTML `onclick` attribute
573
	 */
574
	public static function get_confirm_dialog() {
575 1
576 1
		$confirm = __( 'Are you sure you want to duplicate this entry?', 'gravityview' );
577
578
		/**
579 1
		 * @filter `gravityview/duplicate-entry/confirm-text` Modify the Duplicate Entry Javascript confirmation text (will be sanitized when output)
580
		 *
581 1
		 * @param string $confirm Default: "Are you sure you want to duplicate this entry?". If empty, disable confirmation dialog.
582
		 */
583
		$confirm = apply_filters( 'gravityview/duplicate-entry/confirm-text', $confirm );
584
585
		if ( empty( $confirm ) ) {
586
			return '';
587
		}
588
589
		return 'return window.confirm(\''. esc_js( $confirm ) .'\');';
590
	}
591
592
	/**
593
	 * Check if the user can edit the entry
594
	 *
595
	 * - Is the nonce valid?
596 2
	 * - Does the user have the right caps for the entry
597 2
	 * - Is the entry in the trash?
598
	 *
599 2
	 * @since 2.5
600
	 *
601
	 * @param  array $entry Gravity Forms entry array
602 2
	 * @param  int   $view_id ID of the View being rendered
603
	 *
604 2
	 * @return boolean|WP_Error        True: can edit form. WP_Error: nope.
605
	 */
606 2
	private function user_can_duplicate_entry( $entry = array(), $view_id = null ) {
607
608
		$error = NULL;
609
610
		if ( ! $this->verify_nonce() ) {
611 2
			$error = __( 'The link to duplicate this entry is not valid; it may have expired.', 'gravityview' );
612
		}
613
614
		if ( ! self::check_user_cap_duplicate_entry( $entry, array(), $view_id ) ) {
615
			$error = __( 'You do not have permission to duplicate this entry.', 'gravityview' );
616
		}
617
618
		// No errors; everything's fine here!
619
		if ( empty( $error ) ) {
620
			return true;
621
		}
622
623
		gravityview()->log->error( '{error}', array( 'erorr' => $error ) );
624
625
		return new WP_Error( 'gravityview-duplicate-entry-permissions', $error );
626
	}
627
628
629
	/**
630
	 * checks if user has permissions to view the link or duplicate a specific entry
631
	 *
632
	 * @since 2.5
633
	 *
634
	 * @param  array $entry Gravity Forms entry array
635
	 * @param array $field Field settings (optional)
636
	 * @param int $view_id Pass a View ID to check caps against. If not set, check against current View
637 2
	 *
638
	 * @return bool
639 1
	 */
640
	public static function check_user_cap_duplicate_entry( $entry, $field = array(), $view_id = 0 ) {
641 1
		$current_user = wp_get_current_user();
642
643
		$entry_id = isset( $entry['id'] ) ? $entry['id'] : null;
644
645 2
		// Or if they can duplicate any entries (as defined in Gravity Forms), we're good.
646
		if ( GVCommon::has_cap( array( 'gravityforms_edit_entries', 'gform_full_access', 'gravityview_full_access' ), $entry_id ) ) {
647 1
648
			gravityview()->log->debug( 'Current user has `gravityforms_edit_entries` capability.' );
649
650
			return true;
651 1
		}
652
653 1
654
		// If field options are passed, check if current user can view the link
655 1
		if ( ! empty( $field ) ) {
656
657 1
			// If capability is not defined, something is not right!
658
			if ( empty( $field['allow_duplicate_cap'] ) ) {
659
660
				gravityview()->log->error( 'Cannot read duplicate entry field caps', array( 'data' => $field ) );
661
662 2
				return false;
663
			}
664 1
665
			if ( GVCommon::has_cap( $field['allow_duplicate_cap'] ) ) {
666 1
667
				// Do not return true if cap is read, as we need to check if the current user created the entry
668
				if ( 'read' !== $field['allow_duplicate_cap'] ) {
669 2
					return true;
670
				}
671
672
			} else {
673
674
				gravityview()->log->debug( 'User {user_id} is not authorized to view duplicate entry link ', array( 'user_id' => $current_user->ID ) );
675
676
				return false;
677
			}
678
679
		}
680
681
		if ( ! isset( $entry['created_by'] ) ) {
682
683
			gravityview()->log->error( 'Cannot duplicate entry; entry `created_by` doesn\'t exist.' );
684
685 37
			return false;
686 37
		}
687 37
688
		// Only checks user_duplicate view option if view is already set
689
		if ( $view_id ) {
690
691
			if ( ! $view = \GV\View::by_id( $view_id ) ) {
692
				return false;
693
			}
694
695
			$user_duplicate = $view->settings->get( 'user_duplicate', false );
696
697
			if ( empty( $user_duplicate ) ) {
698
699
				gravityview()->log->debug( 'User Duplicate is disabled. Returning false.' );
700
701
				return false;
702
			}
703
		}
704
705
		// If the logged-in user is the same as the user who created the entry, we're good.
706
		if ( is_user_logged_in() && intval( $current_user->ID ) === intval( $entry['created_by'] ) ) {
707
708
			gravityview()->log->debug( 'User {user_id} created the entry.', array( 'user_id' => $current_user->ID ) );
709
710
			return true;
711
		}
712
713
		return false;
714
	}
715
716
717
	/**
718
	 * After processing duplicate entry, the user will be redirected to the referring View or embedded post/page. Display a message on redirection.
719
	 *
720
	 * If success, there will be `status` URL parameters `status=>success`
721
	 * If an error, there will be `status` and `message` URL parameters `status=>error&message=example`
722
	 *
723
	 * @since 2.5
724
	 *
725
	 * @param int $current_view_id The ID of the View being rendered
726
	 *
727
	 * @return void
728
	 */
729
	public function maybe_display_message( $current_view_id = 0 ) {
730
		if ( empty( $_GET['status'] ) || ! self::verify_nonce() ) {
731
			return;
732
		}
733
734
		// Entry wasn't duplicated from current View
735
		if ( isset( $_GET['view_id'] ) && ( intval( $_GET['view_id'] ) !== intval( $current_view_id ) ) ) {
736
			return;
737
		}
738
739
		$this->display_message();
740
	}
741
742
	public function display_message() {
743
		if ( empty( $_GET['status'] ) || empty( $_GET['duplicate'] ) ) {
744
			return;
745
		}
746
747
		$status = esc_attr( $_GET['status'] );
748
		$message_from_url = \GV\Utils::_GET( 'message' );
749
		$message_from_url = rawurldecode( stripslashes_deep( $message_from_url ) );
750
		$class = '';
751
752
		switch ( $status ) {
753
			case 'error':
754
				$class = ' gv-error error';
755
				$error_message = __( 'There was an error duplicating the entry: %s', 'gravityview' );
756
				$message = sprintf( $error_message, $message_from_url );
757
				break;
758
			default:
759
				$message = __( 'The entry was successfully duplicated.', 'gravityview' );
760
				break;
761
		}
762
763
		/**
764
		 * @filter `gravityview/duplicate-entry/message` Modify the Duplicate Entry messages. Allows HTML; will not be further sanitized.
765
		 * @since 2.5
766
		 * @param string $message Message to be displayed, sanitized using esc_attr()
767
		 * @param string $status Message status (`error` or `success`)
768
		 * @param string $message_from_url The original error message, if any, without the "There was an error duplicating the entry:" prefix
769
		 */
770
		$message = apply_filters( 'gravityview/duplicate-entry/message', esc_attr( $message ), $status, $message_from_url );
771
772
		// DISPLAY ERROR/SUCCESS MESSAGE
773
		echo '<div class="gv-notice' . esc_attr( $class ) .'">'. $message .'</div>';
774
	}
775
776
	/**
777
	 * Add a Duplicate link to the row of actions on the entry list in the backend.
778
	 *
779
	 * @since 2.5.1
780
	 *
781
	 * @param int $form_id The form ID.
782
	 * @param int $field_id The field ID.
783
	 * @param string $value The value.
784
	 * @param array $entry The entry.
785
	 * @param string $query_string The query.
786
	 *
787
	 * @return void
788
	 */
789
	public function make_duplicate_link_row( $form_id, $field_id, $value, $entry, $query_string ) {
0 ignored issues
show
Unused Code introduced by
The parameter $query_string is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
790
791
		/**
792
		 * @filter `gravityview/duplicate/backend/enable` Disables the duplicate link on the backend.
793
		 * @param[in,out] boolean $enable True by default. Enabled.
794
		 * @param int $form_id The form ID.
795
		 */
796
		if ( ! apply_filters( 'gravityview/duplicate/backend/enable', true, $form_id ) ) {
797
			return;
798
		}
799
800
		?>
801
		<span class="gv-duplicate">
802
			|
803
			<a href="<?php echo wp_nonce_url( add_query_arg( 'entry_id', $entry['id'] ), self::get_nonce_key( $entry['id'] ), 'duplicate' ); ?>"><?php esc_html_e( 'Duplicate', 'gravityview' ); ?></a>
804
		</span>
805
		<?php
806
	}
807
808
	/**
809
	 * Perhaps duplicate this entry if the action has been corrected.
810
	 *
811
	 * @since 2.5.1
812
	 *
813
	 * @param int $form_id The form ID.
814
	 *
815
	 * @return void
816
	 */
817
	public function maybe_duplicate_list( $form_id ) {
0 ignored issues
show
Unused Code introduced by
The parameter $form_id is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
818
819
		if ( ! is_admin() ) {
820
			return;
821
		}
822
823
		if ( 'success' === \GV\Utils::_GET( 'result' ) ) {
824
			add_filter( 'gform_admin_messages', function( $messages ) {
825
				$messages = (array) $messages;
826
827
				$messages[] = esc_html__( 'Entry duplicated.', 'gravityview' );
828
				return $messages;
829
			} );
830
		}
831
832
		if ( 'error' === \GV\Utils::_GET( 'result' ) ) {
833
834
			$is_logging_active = class_exists( 'GravityView_Logging' ) ? GravityView_Logging::is_logging_active() : true;
835
836
			$check_logs_message = '';
837
838
			if( $is_logging_active ) {
839
				$check_logs_message = sprintf( ' <a href="%s">%s</a>',
840
					esc_url( admin_url( 'admin.php?page=gf_settings&subview=gravityformslogging' ) ),
841
					esc_html_x( 'Check the GravityView logs for more information.', 'Error message links to logging page', 'gravityview' )
842
				);
843
			}
844
845
			add_filter( 'gform_admin_error_messages', function( $messages ) use ( $check_logs_message ) {
846
				$messages = (array) $messages;
847
848
				$messages[] = esc_html__( 'There was an error duplicating the entry.', 'gravityview' ) . $check_logs_message;
849
850
				return $messages;
851
			} );
852
		}
853
854
		if ( ! wp_verify_nonce( \GV\Utils::_GET( 'duplicate' ), self::get_nonce_key( $entry_id = \GV\Utils::_GET( 'entry_id' ) ) ) ) {
855
			return;
856
		}
857
858
		if ( ! GVCommon::has_cap( array( 'gravityforms_edit_entries', 'gform_full_access', 'gravityview_full_access' ), $entry_id ) ) {
859
			return;
860
		}
861
862
		$entry = GFAPI::get_entry( $entry_id );
863
864
		if ( is_wp_error( $entry ) ) {
865
			$is_duplicated = $entry;
866
		} else {
867
			$is_duplicated = $this->duplicate_entry( $entry );
868
		}
869
870
		if ( is_wp_error( $is_duplicated ) ) {
871
			gravityview()->log->error( 'Error duplicating {id}: {error}', array( 'id' => $entry_id, 'error' => $is_duplicated->get_error_message() ) );
872
		}
873
874
		$return_url = remove_query_arg( 'duplicate' );
875
		$return_url = add_query_arg( 'result', is_wp_error( $is_duplicated ) ? 'error' : 'success', $return_url );
876
877
		echo '<script>window.location.href = ' . json_encode( $return_url ) . ';</script>';
878
879
		exit;
880
	}
881
882
883
} // end class
884
885
GravityView_Duplicate_Entry::getInstance();
886
887