Completed
Pull Request — develop (#1561)
by Zack
36:00 queued 15:58
created

GravityView_Duplicate_Entry::add_reserved_arg()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6

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 6
ccs 0
cts 0
cp 0
crap 2
rs 10
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
		add_filter( 'gravityview/api/reserved_query_args', array( $this, 'add_reserved_arg' ) );
73
	}
74
75
	/**
76
	 * Adds "duplicate" to the list of internal reserved query args
77 2
	 *
78
	 * @since 2.10
79 2
	 *
80 1
	 * @param array $args Existing reserved args
81
	 *
82
	 * @return array
83 2
	 */
84
	public function add_reserved_arg( $args ) {
85
86
		$args[] = 'duplicate';
87
88
		return $args;
89
	}
90
91
	/**
92
	 * Return the instantiated class object
93
	 *
94
	 * @since  2.5
95 1
	 * @return GravityView_Duplicate_Entry
96
	 */
97 1
	static public function getInstance() {
98
99 1
		if ( empty( self::$instance ) ) {
100
			self::$instance = new self;
101 1
		}
102
103
		return self::$instance;
104
	}
105
106
	/**
107
	 * Hide the field or not.
108
	 *
109
	 * For non-logged in users.
110
	 * For users that have no duplicate rights on any of the current entries.
111
	 *
112
	 * @param bool $visible Visible or not.
113 2
	 * @param \GV\Field $field The field.
114
	 * @param \GV\View $view The View context.
115
	 *
116
	 * @return bool
117 2
	 */
118
	public function maybe_not_visible( $visible, $field, $view ) {
119 2
120
		if ( 'duplicate_link' !== $field->ID ) {
121
			return $visible;
122
		}
123
124
		if ( ! $view ) {
125
			return $visible;
126
		}
127
128
		static $visibility_cache_for_view = array();
129
130
		if ( ! is_null( $result = \GV\Utils::get( $visibility_cache_for_view, $view->ID, null ) ) ) {
131
			return $result;
132
		}
133
134
		foreach ( $view->get_entries()->all() as $entry ) {
135
			if ( self::check_user_cap_duplicate_entry( $entry->as_entry(), $field->as_configuration() ) ) {
136
				// At least one entry is duplicable for this user
137
				$visibility_cache_for_view[ $view->ID ] = true;
138
				return true;
139
			}
140
		}
141
142
		$visibility_cache_for_view[ $view->ID ] = false;
143
144
		return false;
145
	}
146
147
	/**
148
	 * Prevent users from being able to sort by the Duplicate field
149
	 *
150
	 * @since 2.8.3
151
	 *
152
	 * @param array $fields Array of field types not editable by users
153
	 *
154
	 * @return array
155
	 */
156
	public function _filter_sortable_fields( $fields ) {
157
158
		$fields = (array) $fields;
159
160
		$fields[] = 'duplicate_link';
161
162
		return $fields;
163
	}
164
165
	/**
166
	 * Include this extension templates path
167
	 *
168
	 * @since  2.5
169
	 *
170
	 * @param array $file_paths List of template paths ordered
171
	 *
172
	 * @return array File paths, with duplicate field path added at index 117
173
	 */
174
	public function add_template_path( $file_paths ) {
175
176
		// Index 100 is the default GravityView template path.
177
		// Index 110 is Edit Entry link
178
		$file_paths[ 117 ] = self::$file;
179
180
		return $file_paths;
181
	}
182
183
	/**
184
	 * Add "Duplicate Link Text" setting to the edit_link field settings
185
	 *
186
	 * @since  2.5
187
	 *
188
	 * @param  array  $field_options [description]
189
	 * @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...
190
	 * @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...
191
	 * @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...
192
	 * @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...
193
	 *
194
	 * @return array                [description]
195
	 */
196
	public function duplicate_link_field_options( $field_options, $template_id, $field_id, $context, $input_type ) {
197
198
		// Always a link, never a filter, always same window
199
		unset( $field_options['show_as_link'], $field_options['search_filter'], $field_options['new_window'] );
200
201
		// Duplicate Entry link should only appear to visitors capable of editing entries
202
		unset( $field_options['only_loggedin'], $field_options['only_loggedin_cap'] );
203
204
		$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...
205
			'type' => 'text',
206
			'label' => __( 'Duplicate Link Text', 'gravityview' ),
207
			'desc' => NULL,
208
			'value' => __( 'Duplicate Entry', 'gravityview' ),
209
			'merge_tags' => true,
210
		);
211
212
		$field_options['allow_duplicate_cap'] = array(
213
			'type' => 'select',
214
			'label' => __( 'Allow the following users to duplicate the entry:', 'gravityview' ),
215
			'choices' => GravityView_Render_Settings::get_cap_choices( $template_id, $field_id, $context, $input_type ),
216
			'tooltip' => 'allow_duplicate_cap',
217
			'class' => 'widefat',
218
			'value' => 'read', // Default: entry creator
219
		);
220
221
		return array_merge( $add_option, $field_options );
222
	}
223
224
225
	/**
226
	 * Add Edit Link as a default field, outside those set in the Gravity Form form
227
	 *
228
	 * @since 2.5
229
	 *
230
	 * @param array $entry_default_fields Existing fields
231
	 * @param  string|array $form form_ID or form object
232
	 * @param  string $zone   Either 'single', 'directory', 'edit', 'header', 'footer'
233
	 *
234
	 * @return array $entry_default_fields, with `duplicate_link` added. Won't be added if in Edit Entry context.
235
	 */
236
	public function add_default_field( $entry_default_fields, $form = array(), $zone = '' ) {
237
238
		if ( 'edit' !== $zone ) {
239
			$entry_default_fields['duplicate_link'] = array(
240
				'label' => __( 'Duplicate Entry', 'gravityview' ),
241
				'type'  => 'duplicate_link',
242
				'desc'  => __( 'A link to duplicate the entry. Respects the Duplicate Entry permissions.', 'gravityview' ),
243
				'icon'  => 'dashicons-controls-repeat',
244
			);
245
		}
246
247
		return $entry_default_fields;
248
	}
249 2
250 2
	/**
251
	 * Add Duplicate Entry Link to the Add Field dialog
252
	 *
253
	 * @since 2.5
254
	 *
255
	 * @param array $available_fields
256
	 *
257
	 * @return array Fields with `duplicate_link` added
258
	 */
259
	public function add_available_field( $available_fields = array() ) {
260
261
		$available_fields['duplicate_link'] = array(
262
			'label_text' => __( 'Duplicate Entry', 'gravityview' ),
263
			'field_id' => 'duplicate_link',
264
			'label_type' => 'field',
265
			'input_type' => 'duplicate_link',
266
			'field_options' => NULL
267 2
		);
268
269 2
		return $available_fields;
270
	}
271 2
272
	/**
273
	 * Change wording for the Edit context to read Entry Creator
274
	 *
275
	 * @since 2.5
276 2
	 *
277 2
	 * @param  array 	    $visibility_caps        Array of capabilities to display in field dropdown.
278 2
	 * @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...
279 2
	 * @param  string       $template_id Table slug
280 2
	 * @param  float|string $field_id    GF Field ID - Example: `3`, `5.2`, `entry_link`, `created_by`
281 2
	 * @param  string       $context     What context are we in? Example: `single` or `directory`
282
	 * @param  string       $input_type  (textarea, list, select, etc.)
283 2
	 *
284
	 * @return array                   Array of field options with `label`, `value`, `type`, `default` keys
285
	 */
286
	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...
287
288
		$caps = $visibility_caps;
289
290
		// If we're configuring fields in the edit context, we want a limited selection
291
		if ( 'duplicate_link' === $field_id ) {
292
293
			// Remove other built-in caps.
294
			unset( $caps['publish_posts'], $caps['gravityforms_view_entries'], $caps['duplicate_others_posts'] );
295
296
			$caps['read'] = _x( 'Entry Creator', 'User capability', 'gravityview' );
297
		}
298
299
		return $caps;
300
	}
301 3
302
	/**
303
	 * Generate a consistent nonce key based on the Entry ID
304 3
	 *
305 3
	 * @since 2.5
306
	 *
307
	 * @param  int $entry_id Entry ID
308
	 *
309 1
	 * @return string           Key used to validate request
310
	 */
311 1
	public static function get_nonce_key( $entry_id ) {
312 1
		return sprintf( 'duplicate_%s', $entry_id );
313 1
	}
314
315
316
	/**
317 1
	 * Generate a nonce link with the base URL of the current View embed
318
	 *
319
	 * We don't want to link to the single entry, because when duplicated, there would be nothing to return to.
320 1
	 *
321
	 * @since 2.5
322 1
	 *
323
	 * @param  array       $entry Gravity Forms entry array
324 1
	 * @param  int         $view_id The View id. Not optional since 2.0
325
	 * @param  int         $post_id ID of the current post/page being embedded on, if any
326 1
	 *
327
	 * @return string|null If directory link is valid, the URL to process the duplicate request. Otherwise, `NULL`.
328
	 */
329 1
	public static function get_duplicate_link( $entry, $view_id, $post_id = null ) {
330 1
331
        $base = GravityView_API::directory_link( $post_id ? : $view_id, true );
332
333
		if ( empty( $base ) ) {
334
			gravityview()->log->error( 'Post ID does not exist: {post_id}', array( 'post_id' => $post_id ) );
335
			return NULL;
336 1
		}
337
338 1
		$actionurl = add_query_arg( array(
339
			'action'	=> 'duplicate',
340
			'entry_id'	=> $entry['id'],
341 1
			'gvid' => $view_id,
342 1
            'view_id' => $view_id,
343
		), $base );
344
345 1
		return add_query_arg( 'duplicate', wp_create_nonce( self::get_nonce_key( $entry['id'] ) ), $actionurl );
346 1
	}
347 1
348 1
	/**
349
	 * Handle the duplication request, if $_GET['action'] is set to "duplicate"
350
	 *
351
	 * 1. Check referrer validity
352
	 * 2. Make sure there's an entry with the slug of $_GET['entry_id']
353
	 * 3. If so, attempt to duplicate the entry. If not, set the error status
354 1
	 * 4. Remove `action=duplicate` from the URL
355
	 * 5. Redirect to the page using `wp_safe_redirect()`
356
	 *
357
	 * @since 2.5
358
	 *
359
	 * @uses wp_safe_redirect()
360
	 *
361
	 * @return void|string $url URL during tests instead of redirect.
362
	 */
363 1
	public function process_duplicate() {
364
365
		// If the form is submitted
366 1
		if ( ( ! isset( $_GET['action'] ) ) || 'duplicate' !== $_GET['action'] || ( ! isset( $_GET['entry_id'] ) ) ) {
367 1
			return;
368
		}
369
370
		// Make sure it's a GravityView request
371 1
		$valid_nonce_key = wp_verify_nonce( \GV\Utils::_GET( 'duplicate' ), self::get_nonce_key( $_GET['entry_id'] ) );
372 1
373
		if ( ! $valid_nonce_key ) {
374 1
			gravityview()->log->debug( 'Duplicate entry not processed: nonce validation failed.' );
375 1
			return;
376
		}
377
378
		// Get the entry slug
379
		$entry_slug = esc_attr( $_GET['entry_id'] );
380
381
		// See if there's an entry there
382
		$entry = gravityview_get_entry( $entry_slug, true, false );
383
384
		if ( $entry ) {
385
386
			$has_permission = $this->user_can_duplicate_entry( $entry );
387
388
			if ( is_wp_error( $has_permission ) ) {
389
390
				$messages = array(
391
					'message' => urlencode( $has_permission->get_error_message() ),
392
					'status' => 'error',
393
				);
394 1
395
			} else {
396 1
397
				// Duplicate the entry
398
				$duplicate_response = $this->duplicate_entry( $entry );
399
400 1
				if ( is_wp_error( $duplicate_response ) ) {
401
402 1
					$messages = array(
403
						'message' => urlencode( $duplicate_response->get_error_message() ),
404 1
						'status' => 'error',
405 1
					);
406
407 1
					gravityview()->log->error( 'Entry {entry_slug} cannot be duplicated: {error_code} {error_message}', array(
408
						'entry_slug'    => $entry_slug,
409
						'error_code'    => $duplicate_response->get_error_code(),
410
						'error_message' => $duplicate_response->get_error_message(),
411 1
					) );
412
413 1
				} else {
414 1
415 1
					$messages = array(
416 1
						'status' => $duplicate_response,
417 1
					);
418 1
419 1
				}
420 1
421 1
			}
422
423
		} else {
424
425
			gravityview()->log->error( 'Duplicate entry failed: there was no entry with the entry slug {entry_slug}', array( 'entry_slug' => $entry_slug ) );
426
427
			$messages = array(
428
				'message' => urlencode( __( 'The entry does not exist.', 'gravityview' ) ),
429 1
				'status' => 'error',
430
			);
431 1
		}
432 1
433
		$redirect_to_base = esc_url_raw( remove_query_arg( array( 'action', 'gvid', 'entry_id' ) ) );
434
		$redirect_to = add_query_arg( $messages, $redirect_to_base );
435 1
436
		if ( defined( 'DOING_GRAVITYVIEW_TESTS' ) ) {
437 1
			return $redirect_to;
438
		}
439 1
440
		wp_safe_redirect( $redirect_to );
441
442 1
		exit();
443 1
	}
444 1
445
	/**
446
	 * Duplicate the entry.
447 1
	 *
448 1
	 * Done after all the checks in self::process_duplicate.
449 1
	 *
450 1
	 * @since 2.5
451 1
	 *
452 1
	 * @param array $entry The entry to be duplicated
453
	 *
454
	 * @return WP_Error|boolean
455
	 */
456
	private function duplicate_entry( $entry ) {
457 1
458
		if ( ! $entry_id = \GV\Utils::get( $entry, 'id' ) ) {
459
			return new WP_Error( 'gravityview-duplicate-entry-missing', __( 'The entry does not exist.', 'gravityview' ) );
460
		}
461
462
		gravityview()->log->debug( 'Starting duplicate entry: {entry_id}', array( 'entry_id' => $entry_id ) );
463
464
		global $wpdb;
465 1
466
		$entry_table = GFFormsModel::get_entry_table_name();
467 1
		$entry_meta_table = GFFormsModel::get_entry_meta_table_name();
468 1
469 1
		if ( ! $row = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM $entry_table WHERE ID = %d", $entry_id ), ARRAY_A ) ) {
470
			return new WP_Error( 'gravityview-duplicate-entry-missing', __( 'The entry does not exist.', 'gravityview' ) );
471 1
		}
472
473
		$form = GFAPI::get_form( $entry['form_id'] );
474
475
		$row['id'] = null;
476 1
		$row['date_created'] = date( 'Y-m-d H:i:s', time() );
477
		$row['date_updated'] = $row['date_created'];
478 1
		$row['is_starred'] = false;
479
		$row['is_read'] = false;
480
		$row['ip'] = rgars( $form, 'personalData/preventIP' ) ? '' : GFFormsModel::get_ip();
481
		$row['source_url'] = esc_url_raw( remove_query_arg( array( 'action', 'gvid', 'result', 'duplicate', 'entry_id' ) ) );
482
		$row['user_agent'] = \GV\Utils::_SERVER( 'HTTP_USER_AGENT' );
483
		$row['created_by'] = wp_get_current_user()->ID;
484
485
		/**
486 1
		 * @filter `gravityview/entry/duplicate/details` Modify the new entry details before it's created.
487
		 * @since 2.5
488 1
		 * @param[in,out] array $row The entry details
489
		 * @param array $entry The original entry
490 1
		 */
491
		$row = apply_filters( 'gravityview/entry/duplicate/details', $row, $entry );
492
493
		if ( ! $wpdb->insert( $entry_table, $row ) ) {
494
			return new WP_Error( 'gravityview-duplicate-entry-db-details', __( 'There was an error duplicating the entry.', 'gravityview' ) );
495
		}
496
497
		$duplicated_id = $wpdb->insert_id;
498
499
		$meta = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM $entry_meta_table WHERE entry_id = %d", $entry_id ), ARRAY_A );
500 1
501
		$duplicate_meta = new WP_List_Util( $meta );
502
503 1
		// Keys that should be reset by default
504
		$reset_meta = array( 'is_approved', 'gravityview_unique_id', 'workflow_current_status_timestamp' );
505
		foreach ( $reset_meta as $meta_key ) {
506
			$duplicate_meta->filter( array( 'meta_key' => $meta_key ), 'NOT' );
507 1
		}
508
509 1
		$save_this_meta = array();
510
		foreach ( $duplicate_meta->get_output() as $m ) {
511
			$save_this_meta[] = array(
512
				'meta_key' => $m['meta_key'],
513
				'meta_value' => $m['meta_value'],
514
				'item_index' => $m['item_index'],
515
			);
516
		}
517
518 1
		// Update the row ID for later usage
519
		$row['id'] = $duplicated_id;
520 1
521
		/**
522
		 * @filter `gravityview/entry/duplicate/meta` Modify the new entry meta details.
523
		 * @param[in,out] array $save_this_meta The duplicate meta. Use/add meta_key, meta_value, item_index.
524
		 * @param array $row The duplicated entry
525
		 * @param array $entry The original entry
526
		 */
527
		$save_this_meta = apply_filters( 'gravityview/entry/duplicate/meta', $save_this_meta, $row, $entry );
528
529
		foreach ( $save_this_meta as $data ) {
530 1
			$data['form_id'] = $entry['form_id'];
531
			$data['entry_id'] = $duplicated_id;
532 1
533
			if ( ! $wpdb->insert( $entry_meta_table, $data ) ) {
534
				return new WP_Error( 'gravityview-duplicate-entry-db-meta', __( 'There was an error duplicating the entry.', 'gravityview' ) );
535
			}
536
		}
537
538
		$duplicated_entry = \GFAPI::get_entry( $duplicated_id );
539 1
540
		$duplicate_response = 'duplicated';
541 1
542 1
		/**
543
		 * @action `gravityview/duplicate-entry/duplicated` Triggered when an entry is duplicated
544
		 * @since 2.5
545 1
		 * @param  array $duplicated_entry The duplicated entry
546
		 * @param  array $entry The original entry
547
		*/
548
		do_action( 'gravityview/duplicate-entry/duplicated', $duplicated_entry, $entry );
549
550
		gravityview()->log->debug( 'Duplicate response: {duplicate_response}', array( 'duplicate_response' => $duplicate_response ) );
551
552
		return $duplicate_response;
553
	}
554
555
	/**
556
	 * Is the current nonce valid for editing the entry?
557
	 *
558
	 * @since 2.5
559
	 *
560
	 * @return boolean
561
	 */
562 1
	public function verify_nonce() {
563
564 1
		// No duplicate entry request was made
565
		if ( empty( $_GET['entry_id'] ) || empty( $_GET['duplicate'] ) ) {
566 1
			return false;
567
		}
568
569
		$nonce_key = self::get_nonce_key( $_GET['entry_id'] );
570 1
571 1
		$valid = wp_verify_nonce( $_GET['duplicate'], $nonce_key );
572
573
		/**
574
		 * @filter `gravityview/duplicate-entry/verify_nonce` Override Duplicate Entry nonce validation. Return true to declare nonce valid.
575 1
		 * @since 2.5
576 1
		 * @see wp_verify_nonce()
577
		 * @param int|boolean $valid False if invalid; 1 or 2 when nonce was generated
578
		 * @param string $nonce_key Name of nonce action used in wp_verify_nonce. $_GET['duplicate'] holds the nonce value itself. Default: `duplicate_{entry_id}`
579 1
		 */
580
		$valid = apply_filters( 'gravityview/duplicate-entry/verify_nonce', $valid, $nonce_key );
581 1
582
		return $valid;
583
	}
584
585
	/**
586
	 * Get the onclick attribute for the confirm dialogs that warns users before they duplicate an entry
587
	 *
588
	 * @since 2.5
589
	 *
590
	 * @return string HTML `onclick` attribute
591
	 */
592
	public static function get_confirm_dialog() {
593
594
		$confirm = __( 'Are you sure you want to duplicate this entry?', 'gravityview' );
595
596 2
		/**
597 2
		 * @filter `gravityview/duplicate-entry/confirm-text` Modify the Duplicate Entry Javascript confirmation text (will be sanitized when output)
598
		 *
599 2
		 * @param string $confirm Default: "Are you sure you want to duplicate this entry?". If empty, disable confirmation dialog.
600
		 */
601
		$confirm = apply_filters( 'gravityview/duplicate-entry/confirm-text', $confirm );
602 2
603
		if ( empty( $confirm ) ) {
604 2
			return '';
605
		}
606 2
607
		return 'return window.confirm(\''. esc_js( $confirm ) .'\');';
608
	}
609
610
	/**
611 2
	 * Check if the user can edit the entry
612
	 *
613
	 * - Is the nonce valid?
614
	 * - Does the user have the right caps for the entry
615
	 * - Is the entry in the trash?
616
	 *
617
	 * @since 2.5
618
	 *
619
	 * @param  array $entry Gravity Forms entry array
620
	 * @param  int   $view_id ID of the View being rendered
621
	 *
622
	 * @return boolean|WP_Error        True: can edit form. WP_Error: nope.
623
	 */
624
	private function user_can_duplicate_entry( $entry = array(), $view_id = null ) {
625
626
		$error = NULL;
627
628
		if ( ! $this->verify_nonce() ) {
629
			$error = __( 'The link to duplicate this entry is not valid; it may have expired.', 'gravityview' );
630
		}
631
632
		if ( ! self::check_user_cap_duplicate_entry( $entry, array(), $view_id ) ) {
633
			$error = __( 'You do not have permission to duplicate this entry.', 'gravityview' );
634
		}
635
636
		// No errors; everything's fine here!
637 2
		if ( empty( $error ) ) {
638
			return true;
639 1
		}
640
641 1
		gravityview()->log->error( '{error}', array( 'erorr' => $error ) );
642
643
		return new WP_Error( 'gravityview-duplicate-entry-permissions', $error );
644
	}
645 2
646
647 1
	/**
648
	 * checks if user has permissions to view the link or duplicate a specific entry
649
	 *
650
	 * @since 2.5
651 1
	 *
652
	 * @param  array $entry Gravity Forms entry array
653 1
	 * @param array $field Field settings (optional)
654
	 * @param int $view_id Pass a View ID to check caps against. If not set, check against current View
655 1
	 *
656
	 * @return bool
657 1
	 */
658
	public static function check_user_cap_duplicate_entry( $entry, $field = array(), $view_id = 0 ) {
659
		$current_user = wp_get_current_user();
660
661
		$entry_id = isset( $entry['id'] ) ? $entry['id'] : null;
662 2
663
		// Or if they can duplicate any entries (as defined in Gravity Forms), we're good.
664 1
		if ( GVCommon::has_cap( array( 'gravityforms_edit_entries', 'gform_full_access', 'gravityview_full_access' ), $entry_id ) ) {
665
666 1
			gravityview()->log->debug( 'Current user has `gravityforms_edit_entries` capability.' );
667
668
			return true;
669 2
		}
670
671
672
		// If field options are passed, check if current user can view the link
673
		if ( ! empty( $field ) ) {
674
675
			// If capability is not defined, something is not right!
676
			if ( empty( $field['allow_duplicate_cap'] ) ) {
677
678
				gravityview()->log->error( 'Cannot read duplicate entry field caps', array( 'data' => $field ) );
679
680
				return false;
681
			}
682
683
			if ( GVCommon::has_cap( $field['allow_duplicate_cap'] ) ) {
684
685 37
				// Do not return true if cap is read, as we need to check if the current user created the entry
686 37
				if ( 'read' !== $field['allow_duplicate_cap'] ) {
687 37
					return true;
688
				}
689
690
			} else {
691
692
				gravityview()->log->debug( 'User {user_id} is not authorized to view duplicate entry link ', array( 'user_id' => $current_user->ID ) );
693
694
				return false;
695
			}
696
697
		}
698
699
		if ( ! isset( $entry['created_by'] ) ) {
700
701
			gravityview()->log->error( 'Cannot duplicate entry; entry `created_by` doesn\'t exist.' );
702
703
			return false;
704
		}
705
706
		// Only checks user_duplicate view option if view is already set
707
		if ( $view_id ) {
708
709
			if ( ! $view = \GV\View::by_id( $view_id ) ) {
710
				return false;
711
			}
712
713
			$user_duplicate = $view->settings->get( 'user_duplicate', false );
714
715
			if ( empty( $user_duplicate ) ) {
716
717
				gravityview()->log->debug( 'User Duplicate is disabled. Returning false.' );
718
719
				return false;
720
			}
721
		}
722
723
		// If the logged-in user is the same as the user who created the entry, we're good.
724
		if ( is_user_logged_in() && intval( $current_user->ID ) === intval( $entry['created_by'] ) ) {
725
726
			gravityview()->log->debug( 'User {user_id} created the entry.', array( 'user_id' => $current_user->ID ) );
727
728
			return true;
729
		}
730
731
		return false;
732
	}
733
734
735
	/**
736
	 * After processing duplicate entry, the user will be redirected to the referring View or embedded post/page. Display a message on redirection.
737
	 *
738
	 * If success, there will be `status` URL parameters `status=>success`
739
	 * If an error, there will be `status` and `message` URL parameters `status=>error&message=example`
740
	 *
741
	 * @since 2.5
742
	 *
743
	 * @param int $current_view_id The ID of the View being rendered
744
	 *
745
	 * @return void
746
	 */
747
	public function maybe_display_message( $current_view_id = 0 ) {
748
		if ( empty( $_GET['status'] ) || ! self::verify_nonce() ) {
749
			return;
750
		}
751
752
		// Entry wasn't duplicated from current View
753
		if ( isset( $_GET['view_id'] ) && ( intval( $_GET['view_id'] ) !== intval( $current_view_id ) ) ) {
754
			return;
755
		}
756
757
		$this->display_message();
758
	}
759
760
	public function display_message() {
761
		if ( empty( $_GET['status'] ) || empty( $_GET['duplicate'] ) ) {
762
			return;
763
		}
764
765
		$status = esc_attr( $_GET['status'] );
766
		$message_from_url = \GV\Utils::_GET( 'message' );
767
		$message_from_url = rawurldecode( stripslashes_deep( $message_from_url ) );
768
		$class = '';
769
770
		switch ( $status ) {
771
			case 'error':
772
				$class = ' gv-error error';
773
				$error_message = __( 'There was an error duplicating the entry: %s', 'gravityview' );
774
				$message = sprintf( $error_message, $message_from_url );
775
				break;
776
			default:
777
				$message = __( 'The entry was successfully duplicated.', 'gravityview' );
778
				break;
779
		}
780
781
		/**
782
		 * @filter `gravityview/duplicate-entry/message` Modify the Duplicate Entry messages. Allows HTML; will not be further sanitized.
783
		 * @since 2.5
784
		 * @param string $message Message to be displayed, sanitized using esc_attr()
785
		 * @param string $status Message status (`error` or `success`)
786
		 * @param string $message_from_url The original error message, if any, without the "There was an error duplicating the entry:" prefix
787
		 */
788
		$message = apply_filters( 'gravityview/duplicate-entry/message', esc_attr( $message ), $status, $message_from_url );
789
790
		// DISPLAY ERROR/SUCCESS MESSAGE
791
		echo '<div class="gv-notice' . esc_attr( $class ) .'">'. $message .'</div>';
792
	}
793
794
	/**
795
	 * Add a Duplicate link to the row of actions on the entry list in the backend.
796
	 *
797
	 * @since 2.5.1
798
	 *
799
	 * @param int $form_id The form ID.
800
	 * @param int $field_id The field ID.
801
	 * @param string $value The value.
802
	 * @param array $entry The entry.
803
	 * @param string $query_string The query.
804
	 *
805
	 * @return void
806
	 */
807
	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...
808
809
		/**
810
		 * @filter `gravityview/duplicate/backend/enable` Disables the duplicate link on the backend.
811
		 * @param[in,out] boolean $enable True by default. Enabled.
812
		 * @param int $form_id The form ID.
813
		 */
814
		if ( ! apply_filters( 'gravityview/duplicate/backend/enable', true, $form_id ) ) {
815
			return;
816
		}
817
818
		?>
819
		<span class="gv-duplicate">
820
			|
821
			<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>
822
		</span>
823
		<?php
824
	}
825
826
	/**
827
	 * Perhaps duplicate this entry if the action has been corrected.
828
	 *
829
	 * @since 2.5.1
830
	 *
831
	 * @param int $form_id The form ID.
832
	 *
833
	 * @return void
834
	 */
835
	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...
836
837
		if ( ! is_admin() ) {
838
			return;
839
		}
840
841
		if ( 'success' === \GV\Utils::_GET( 'result' ) ) {
842
			add_filter( 'gform_admin_messages', function( $messages ) {
843
				$messages = (array) $messages;
844
845
				$messages[] = esc_html__( 'Entry duplicated.', 'gravityview' );
846
				return $messages;
847
			} );
848
		}
849
850
		if ( 'error' === \GV\Utils::_GET( 'result' ) ) {
851
852
			$is_logging_active = class_exists( 'GravityView_Logging' ) ? GravityView_Logging::is_logging_active() : true;
853
854
			$check_logs_message = '';
855
856
			if( $is_logging_active ) {
857
				$check_logs_message = sprintf( ' <a href="%s">%s</a>',
858
					esc_url( admin_url( 'admin.php?page=gf_settings&subview=gravityformslogging' ) ),
859
					esc_html_x( 'Check the GravityView logs for more information.', 'Error message links to logging page', 'gravityview' )
860
				);
861
			}
862
863
			add_filter( 'gform_admin_error_messages', function( $messages ) use ( $check_logs_message ) {
864
				$messages = (array) $messages;
865
866
				$messages[] = esc_html__( 'There was an error duplicating the entry.', 'gravityview' ) . $check_logs_message;
867
868
				return $messages;
869
			} );
870
		}
871
872
		if ( ! wp_verify_nonce( \GV\Utils::_GET( 'duplicate' ), self::get_nonce_key( $entry_id = \GV\Utils::_GET( 'entry_id' ) ) ) ) {
873
			return;
874
		}
875
876
		if ( ! GVCommon::has_cap( array( 'gravityforms_edit_entries', 'gform_full_access', 'gravityview_full_access' ), $entry_id ) ) {
877
			return;
878
		}
879
880
		$entry = GFAPI::get_entry( $entry_id );
881
882
		if ( is_wp_error( $entry ) ) {
883
			$is_duplicated = $entry;
884
		} else {
885
			$is_duplicated = $this->duplicate_entry( $entry );
886
		}
887
888
		if ( is_wp_error( $is_duplicated ) ) {
889
			gravityview()->log->error( 'Error duplicating {id}: {error}', array( 'id' => $entry_id, 'error' => $is_duplicated->get_error_message() ) );
890
		}
891
892
		$return_url = remove_query_arg( 'duplicate' );
893
		$return_url = add_query_arg( 'result', is_wp_error( $is_duplicated ) ? 'error' : 'success', $return_url );
894
895
		echo '<script>window.location.href = ' . json_encode( $return_url ) . ';</script>';
896
897
		exit;
898
	}
899
900
901
} // end class
902
903
GravityView_Duplicate_Entry::getInstance();
904
905