Completed
Push — develop ( 1c83ad...4f9882 )
by Zack
26:36 queued 12:16
created

GravityView_Field::add_entry_meta()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 22

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 4.128

Importance

Changes 0
Metric Value
cc 4
nc 3
nop 1
dl 0
loc 22
ccs 8
cts 10
cp 0.8
crap 4.128
rs 9.568
c 0
b 0
f 0
1
<?php
2
/**
3
 * @file class-gravityview-field.php
4
 * @package GravityView
5
 * @subpackage includes\fields
6
 */
7
8
/**
9
 * Modify field settings by extending this class.
10
 */
11
abstract class GravityView_Field {
12
13
	/**
14
	 * The name of the GravityView field type
15
	 * Example: `created_by`, `text`, `fileupload`, `address`, `entry_link`
16
	 * @var string
17
	 */
18
	public $name;
19
20
	/**
21
	 * @internal Not yet implemented
22
	 * @since 1.15.2
23
	 * @type string The description of the field in the field picker
24
	 */
25
	public $description;
26
27
	/**
28
	 * @since 1.15.2
29
	 * @type string The label of the field in the field picker
30
	 */
31
	public $label;
32
33
	/**
34
	 * @var string The default search label used by the search widget, if not set
35
	 */
36
	public $default_search_label;
37
38
	/**
39
	 * `standard`, `advanced`, `post`, `pricing`, `meta`, `gravityview`
40
	 * @since 1.15.2
41
	 * @type string The group belongs to this field in the field picker
42
	 */
43
	public $group;
44
45
	/**
46
	 * @internal Not yet implemented
47
	 * @type boolean Can the field be searched?
48
	 * @since 1.15.2
49
	 */
50
	public $is_searchable = true;
51
52
	/**
53
	 * @internal Not yet implemented
54
	 * @type array $search_operators The type of search operators available for this field
55
	 * @since 1.15.2
56
	 */
57
	public $search_operators;
58
59
	/**
60
	 * @type boolean Can the field be sorted in search?
61
	 * @since 1.15.2
62
	 */
63
	public $is_sortable = true;
64
65
	/**
66
	 * @type boolean Is field content number-based?
67
	 * @since 1.15.2
68
	 */
69
	public $is_numeric;
70
71
	/**
72
	 * @var null|string The key used to search and sort entry meta in Gravity Forms. Used if the field stores data as custom entry meta.
73
	 * @see https://www.gravityhelp.com/documentation/article/gform_entry_meta/
74
	 * @since 1.19
75
	 */
76
	public $entry_meta_key = null;
77
78
	/**
79
	 * @var string|array Optional. The callback function after entry meta is updated, only used if $entry_meta_key is set.
80
	 * @see https://www.gravityhelp.com/documentation/article/gform_entry_meta/
81
	 * @since 1.19
82
	 */
83
	var $entry_meta_update_callback = null;
84
85
	/**
86
	 * @var bool Whether to show meta when set to true automatically adds the column to the entry list, without having to edit and add the column for display
87
	 * @since 1.19
88
	 */
89
	var $entry_meta_is_default_column = false;
90
91
	/**
92
	 * @internal Not yet implemented
93
	 * @todo implement supports_context() method
94
	 * The contexts in which a field is available. Some fields aren't editable, for example.
95
	 * - `singular` is an alias for both `single` and `edit`
96
	 * - `multiple` is an alias for `directory` (backward compatibility)
97
	 * @type array
98
	 * @since 1.15.2
99
	 */
100
	public $contexts = array( 'single', 'multiple', 'edit', 'export' );
101
102
	/**
103
	 * @since 1.15.2
104
	 * @since 1.16.2.2 Changed access to public (previously, protected)
105
	 * @type string The name of a corresponding Gravity Forms GF_Field class, if exists
106
	 */
107
	public $_gf_field_class_name;
108
109
	/**
110
	 * @var string The field ID being requested
111
	 * @since 1.14
112
	 */
113
	protected $_field_id = '';
114
115
	/**
116
	 * @var string Field options to be rendered
117
	 * @since 1.14
118
	 */
119
	protected $_field_options = array();
120
121
	/**
122
	 * @var bool|string Name of merge tag (without curly brackets), if the field has custom GravityView merge tags to add. Otherwise, false.
123
	 * @since 1.16
124
	 */
125
	protected $_custom_merge_tag = false;
126
127
	/**
128
	 * GravityView_Field constructor.
129
	 */
130
	public function __construct() {
131
132
		// Modify the field options based on the name of the field type
133
		add_filter( sprintf( 'gravityview_template_%s_options', $this->name ), array( &$this, 'field_options' ), 10, 5 );
134
135
		add_filter( 'gravityview/sortable/field_blacklist', array( $this, '_filter_sortable_fields' ), 1 );
136
137
		if( $this->entry_meta_key ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->entry_meta_key of type null|string is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
138
			add_filter( 'gform_entry_meta', array( $this, 'add_entry_meta' ) );
139
			add_filter( 'gravityview/common/sortable_fields', array( $this, 'add_sortable_field' ), 10, 2 );
140
		}
141
142
		if( $this->_custom_merge_tag ) {
143
			add_filter( 'gform_custom_merge_tags', array( $this, '_filter_gform_custom_merge_tags' ), 10, 4 );
144
			add_filter( 'gform_replace_merge_tags', array( $this, '_filter_gform_replace_merge_tags' ), 10, 7 );
145
		}
146
147
		if( 'meta' === $this->group || '' !== $this->default_search_label ) {
148
			add_filter( 'gravityview_search_field_label', array( $this, 'set_default_search_label' ), 10, 3 );
149
		}
150
151
		/**
152
		 * Auto-assign label from Gravity Forms label, if exists
153
		 * @since 1.20
154
		 */
155
		if( empty( $this->label ) && ! empty( $this->_gf_field_class_name ) && class_exists( $this->_gf_field_class_name ) ) {
156
			$this->label = ucfirst( GF_Fields::get( $this->name )->get_form_editor_field_title() );
157
		}
158
159
		GravityView_Fields::register( $this );
160
	}
161
162
	/**
163
	 * Add the field to the Filter & Sort available fields
164
	 *
165
	 * @since 1.19
166
	 *
167
	 * @param array $fields Sub-set of GF form fields that are sortable
168
	 *
169
	 * @return array Modified $fields array to include approval status in the sorting dropdown
170
	 */
171
	public function add_sortable_field( $fields ) {
172
173
		$added_field = array(
174
			'label' => $this->label,
175
			'type'  => $this->name
176
		);
177
178
		$fields["{$this->entry_meta_key}"] = $added_field;
179
180
		return $fields;
181
	}
182
183
	/**
184
	 * Allow setting a default search label for search fields based on the field type
185
	 *
186
	 * Useful for entry meta "fields" that don't have Gravity Forms labels, like `created_by`
187
	 *
188
	 * @since 1.17.3
189
	 *
190
	 * @param string $label Existing label text, sanitized.
191
	 * @param array $gf_field Gravity Forms field array, as returned by `GFFormsModel::get_field()`
192
	 * @param array $field Field setting as sent by the GV configuration - has `field`, `input` (input type), and `label` keys
193
	 *
194
	 * @return string
195
	 */
196 4
	function set_default_search_label( $label = '', $gf_field = null, $field = 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...
197
198 4
		if( $this->name === $field['field'] && '' === $label ) {
199
			$label = esc_html( $this->default_search_label );
200
		}
201
202 4
		return $label;
203
	}
204
205
	/**
206
	 * Match the merge tag in replacement text for the field.  DO NOT OVERRIDE.
207
	 *
208
	 * @see replace_merge_tag Override replace_merge_tag() to handle any matches
209
	 *
210
	 * @since 1.16
211
	 *
212
	 * @param string $text Text to replace
213
	 * @param array $form Gravity Forms form array
214
	 * @param array $entry Entry array
215
	 * @param bool $url_encode Whether to URL-encode output
216
	 *
217
	 * @return string Original text if {_custom_merge_tag} isn't found. Otherwise, replaced text.
218
	 */
219 42
	public function _filter_gform_replace_merge_tags( $text, $form = array(), $entry = array(), $url_encode = false, $esc_html = false  ) {
220
221
		// Is there is field merge tag? Strip whitespace off the ned, too.
222 42
		preg_match_all( '/{' . preg_quote( $this->_custom_merge_tag ) . ':?(.*?)(?:\s)?}/ism', $text, $matches, PREG_SET_ORDER );
223
224
		// If there are no matches, return original text
225 42
		if ( empty( $matches ) ) {
226 42
			return $text;
227
		}
228
229 2
		return $this->replace_merge_tag( $matches, $text, $form, $entry, $url_encode, $esc_html );
230
	}
231
232
	/**
233
	 * Run GravityView filters when using GFCommon::replace_variables()
234
	 *
235
	 * Instead of adding multiple hooks, add all hooks into this one method to improve speed
236
	 *
237
	 * @since 1.8.4
238
	 *
239
	 * @see GFCommon::replace_variables()
240
	 *
241
	 * @param array $matches Array of Merge Tag matches found in text by preg_match_all
242
	 * @param string $text Text to replace
243
	 * @param array|bool $form Gravity Forms form array. When called inside {@see GFCommon::replace_variables()} (now deprecated), `false`
244
	 * @param array|bool $entry Entry array.  When called inside {@see GFCommon::replace_variables()} (now deprecated), `false`
245
	 * @param bool $url_encode Whether to URL-encode output
246
	 * @param bool $esc_html Whether to apply `esc_html()` to output
247
	 *
248
	 * @return mixed
249
	 */
250 1
	public function replace_merge_tag( $matches = array(), $text = '', $form = array(), $entry = array(), $url_encode = false, $esc_html = false ) {
0 ignored issues
show
Unused Code introduced by
The parameter $form 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...
251
252 1
		foreach( $matches as $match ) {
253
254 1
			$full_tag = $match[0];
255
256
			// Strip the Merge Tags
257 1
			$tag = str_replace( array( '{', '}'), '', $full_tag );
258
259
			// Replace the value from the entry, if exists
260 1
			if( isset( $entry[ $tag ] ) ) {
261
262 1
				$value = $entry[ $tag ];
263
264 1
				if( is_callable( array( $this, 'get_content') ) ) {
265 1
					$value = $this->get_content( $value );
0 ignored issues
show
Bug introduced by
It seems like you code against a specific sub-type and not the parent class GravityView_Field as the method get_content() does only exist in the following sub-classes of GravityView_Field: GravityView_Field_Date_Created, GravityView_Field_Date_Updated, GravityView_Field_Is_Fulfilled, GravityView_Field_Is_Starred, GravityView_Field_Payment_Amount, GravityView_Field_Payment_Date, GravityView_Field_Transaction_Type. Maybe you want to instanceof check for one of these explicitly?

Let’s take a look at an example:

abstract class User
{
    /** @return string */
    abstract public function getPassword();
}

class MyUser extends User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different sub-classes of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the parent class:

    abstract class User
    {
        /** @return string */
        abstract public function getPassword();
    
        /** @return string */
        abstract public function getDisplayName();
    }
    
Loading history...
266
				}
267
268 1
				$text = str_replace( $full_tag, $value, $text );
269
			}
270
		}
271
272 1
		unset( $value, $tag, $full_tag );
273
274 1
		return $text;
275
	}
276
277
	/**
278
	 * Add custom merge tags to merge tag options. DO NOT OVERRIDE.
279
	 *
280
	 * @internal Not to be overridden by fields
281
	 *
282
	 * @since 1.8.4
283
	 *
284
	 * @param array $custom_merge_tags
285
	 * @param int $form_id GF Form ID
286
	 * @param GF_Field[] $fields Array of fields in the form
287
	 * @param string $element_id The ID of the input that Merge Tags are being used on
288
	 *
289
	 * @return array Modified merge tags
290
	 */
291
	public function _filter_gform_custom_merge_tags( $custom_merge_tags = array(), $form_id, $fields = array(), $element_id = '' ) {
0 ignored issues
show
Unused Code introduced by
The parameter $element_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...
292
293
		$form = GVCommon::get_form( $form_id );
294
295
		$field_merge_tags = $this->custom_merge_tags( $form, $fields );
296
297
		return array_merge( $custom_merge_tags, $field_merge_tags );
298
	}
299
300
	/**
301
	 * Add custom Merge Tags to Merge Tag options, if custom Merge Tags exist
302
	 *
303
	 * Should be overridden if there's more than one Merge Tag to add or if the Merge Tag isn't {_custom_merge_tag}
304
	 *
305
	 * @since 1.16
306
	 *
307
	 * @param array $form GF Form array
308
	 * @param GF_Field[] $fields Array of fields in the form
309
	 *
310
	 * @return array Merge tag array with `label` and `tag` keys based on class `label` and `_custom_merge_tag` variables
311
	 */
312
	protected function custom_merge_tags( $form = array(), $fields = array() ) {
0 ignored issues
show
Unused Code introduced by
The parameter $form 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 $fields 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...
313
314
		// Use variables to make it unnecessary for other fields to override
315
		$merge_tags = array(
316
			array(
317
				'label' => $this->label,
318
				'tag' => '{' . $this->_custom_merge_tag . '}',
319
			),
320
		);
321
322
		return $merge_tags;
323
	}
324
325
	/**
326
	 * Use field settings to modify whether a field is sortable
327
	 *
328
	 * @see GravityView_frontend::is_field_sortable
329
	 * @since 1.15.3
330
	 *
331
	 * @param array $not_sortable Existing field types that aren't sortable
332
	 *
333
	 * @return array
334
	 */
335 1
	public function _filter_sortable_fields( $not_sortable ) {
336
337 1
		if( ! $this->is_sortable ) {
338 1
			$not_sortable[] = $this->name;
339
		}
340
341 1
		return $not_sortable;
342
	}
343
344
	/**
345
	 * Add the custom entry meta key to make it searchable and sortable
346
	 *
347
	 * @see https://www.gravityhelp.com/documentation/article/gform_entry_meta/
348
	 *
349
	 * @param array $entry_meta Array of custom entry meta keys with associative arrays
350
	 *
351
	 * @return mixed
352
	 */
353 144
	function add_entry_meta( $entry_meta ) {
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...
354
355 144
		if( ! isset( $entry_meta["{$this->entry_meta_key}"] ) ) {
356
357
			$added_meta = array(
358 144
				'label'             => $this->label,
359 144
				'is_numeric'        => $this->is_numeric,
360 144
				'is_default_column' => $this->entry_meta_is_default_column,
361
			);
362
363 144
			if ( $this->entry_meta_update_callback && is_callable( $this->entry_meta_update_callback ) ) {
364
				$added_meta['update_entry_meta_callback'] = $this->entry_meta_update_callback;
365
			}
366
367 144
			$entry_meta["{$this->entry_meta_key}"] = $added_meta;
368
369
		} else {
370
			gravityview()->log->error( 'Entry meta already set: {meta_key}', array( 'meta_key' => $this->entry_meta_key, 'data' =>  $entry_meta["{$this->entry_meta_key}"] ) );
371
		}
372
373 144
		return $entry_meta;
374
	}
375
376
	private function field_support_options() {
377
		$options = array(
378
			'link_to_post' => array(
379
				'type' => 'checkbox',
380
				'label' => __( 'Link to the post', 'gravityview' ),
381
				'desc' => __( 'Link to the post created by the entry.', 'gravityview' ),
382
				'value' => false,
383
			),
384
			'link_to_term' => array(
385
				'type' => 'checkbox',
386
				'label' => __( 'Link to the category or tag', 'gravityview' ),
387
				'desc' => __( 'Link to the current category or tag. "Link to single entry" must be unchecked.', 'gravityview' ),
388
				'value' => false,
389
			),
390
			'dynamic_data' => array(
391
				'type' => 'checkbox',
392
				'label' => __( 'Use the live post data', 'gravityview' ),
393
				'desc' => __( 'Instead of using the entry data, instead use the current post data.', 'gravityview' ),
394
				'value' => true,
395
			),
396
			'date_display' => array(
397
				'type' => 'text',
398
				'label' => __( 'Override Date Format', 'gravityview' ),
399
				'desc' => sprintf( __( 'Define how the date is displayed (using %sthe PHP date format%s)', 'gravityview'), '<a href="https://codex.wordpress.org/Formatting_Date_and_Time">', '</a>' ),
400
				/**
401
				 * @filter `gravityview_date_format` Override the date format with a [PHP date format](https://codex.wordpress.org/Formatting_Date_and_Time)
402
				 * @param[in,out] null|string $date_format Date Format (default: null)
403
				 */
404
				'value' => apply_filters( 'gravityview_date_format', null )
405
			),
406
			'new_window' => array(
407
				'type' => 'checkbox',
408
				'label' => __( 'Open link in a new tab or window?', 'gravityview' ),
409
				'value' => false,
410
			),
411
		);
412
413
		/**
414
		 * @filter `gravityview_field_support_options` Modify the settings that a field supports
415
		 * @param array $options Options multidimensional array with each key being the input name, with each array setting having `type`, `label`, `desc` and `value` (default values) keys
416
		 */
417
		return apply_filters( 'gravityview_field_support_options', $options );
418
	}
419
420
	function add_field_support( $key = '', &$field_options ) {
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...
421
422
		$options = $this->field_support_options();
423
424
		if( isset( $options[ $key ] ) ) {
425
			$field_options[ $key ] = $options[ $key ];
426
		}
427
428
		return $field_options;
429
	}
430
431
	/**
432
	 * Tap in here to modify field options.
433
	 *
434
	 * Here's an example:
435
	 *
436
	 * <pre>
437
	 * $field_options['name_display'] = array(
438
	 * 	'type' => 'select',
439
	 * 	'label' => __( 'User Format', 'gravityview' ),
440
	 * 	'desc' => __( 'How should the User information be displayed?', 'gravityview'),
441
	 * 	'choices' => array(
442
	 * 		array(
443
	 *		 	'value' => 'display_name',
444
	 *		  	'label' => __('Display Name (Example: "Ellen Ripley")', 'gravityview'),
445
	 *		),
446
	 *  	array(
447
	 *			'value' => 'user_login',
448
	 *			'label' => __('Username (Example: "nostromo")', 'gravityview')
449
	 *		),
450
	 * 	 'value' => 'display_name'
451
	 * );
452
	 * </pre>
453
	 *
454
	 * @param  array      $field_options [description]
455
	 * @param  string      $template_id   [description]
456
	 * @param  string      $field_id      [description]
457
	 * @param  string      $context       [description]
458
	 * @param  string      $input_type    [description]
459
	 * @return array                     [description]
460
	 */
461
	public function field_options( $field_options, $template_id, $field_id, $context, $input_type ) {
462
463
		$this->_field_options = $field_options;
464
		$this->_field_id = $field_id;
465
466
		return $field_options;
467
	}
468
469
	/**
470
	 * Check whether the `enableChoiceValue` flag is set for a GF field
471
	 *
472
	 * Gets the current form ID, gets the field at that ID, then checks for the enableChoiceValue value.
473
	 *
474
	 * @access protected
475
	 *
476
	 * @uses GFAPI::get_form
477
	 *
478
	 * @since 1.17
479
	 *
480
	 * @return bool True: Enable Choice Value is active for field; False: not active, or form invalid, or form not found.
481
	 */
482
	protected function is_choice_value_enabled() {
483
484
		// If "Add Field" button is processing, get the Form ID
485
		$connected_form = \GV\Utils::_POST( 'form_id' );
486
487
		// Otherwise, get the Form ID from the Post page
488
		if( empty( $connected_form ) ) {
489
			$connected_form = gravityview_get_form_id( get_the_ID() );
490
		}
491
492
		if( empty( $connected_form ) ) {
493
			gravityview()->log->error( 'Form not found for form ID "{form_id}"', array( 'form_id' => $connected_form ) );
494
			return false;
495
		}
496
497
		$form = GFAPI::get_form( $connected_form );
498
499
		if ( ! $form ) {
500
			gravityview()->log->error( 'Form not found for field ID of "{field_id}", when checking for a form with ID of "{form_id}"', array( 'field_id' => $this->_field_id, 'form_id' => $connected_form ) );
501
			return false;
502
		}
503
504
		$field = gravityview_get_field( $form, $this->_field_id );
505
506
		return ! empty( $field->enableChoiceValue );
507
	}
508
509
}
510