Completed
Push — develop ( 846913...22c9fc )
by Zack
17s queued 12s
created

GravityView_Widget_Search::add_reserved_args()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 20

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 3

Importance

Changes 0
Metric Value
cc 3
nc 3
nop 1
dl 0
loc 20
ccs 6
cts 6
cp 1
crap 3
rs 9.6
c 0
b 0
f 0
1
<?php
2
/**
3
 * The GravityView New Search widget
4
 *
5
 * @package   GravityView-DataTables-Ext
6
 * @license   GPL2+
7
 * @author    GravityView <[email protected]>
8
 * @link      http://gravityview.co
9
 * @copyright Copyright 2014, Katz Web Services, Inc.
10
 */
11
12
if ( ! defined( 'WPINC' ) ) {
13
	die;
14
}
15
16
class GravityView_Widget_Search extends \GV\Widget {
17
18
	public static $file;
19
	public static $instance;
20
21
	private $search_filters = array();
22
23
	/**
24
	 * whether search method is GET or POST ( default: GET )
25
	 * @since 1.16.4
26
	 * @var string $search_method
27
	 */
28
	private $search_method = 'get';
29
30 39
	public function __construct() {
31
32 39
		$this->widget_id = 'search_bar';
33 39
		$this->widget_description = esc_html__( 'Search form for searching entries.', 'gravityview' );
34
35 39
		self::$instance = &$this;
36
37 39
		self::$file = plugin_dir_path( __FILE__ );
38
39 39
		$default_values = array( 'header' => 0, 'footer' => 0 );
40
41
		$settings = array(
42 39
			'search_layout' => array(
43 39
				'type' => 'radio',
44
				'full_width' => true,
45 39
				'label' => esc_html__( 'Search Layout', 'gravityview' ),
46 39
				'value' => 'horizontal',
47
				'options' => array(
48 39
					'horizontal' => esc_html__( 'Horizontal', 'gravityview' ),
49 39
					'vertical' => esc_html__( 'Vertical', 'gravityview' ),
50
				),
51
			),
52
			'search_clear' => array(
53 39
				'type' => 'checkbox',
54 39
				'label' => __( 'Show Clear button', 'gravityview' ),
55
				'value' => false,
56
			),
57
			'search_fields' => array(
58
				'type' => 'hidden',
59
				'label' => '',
60
				'class' => 'gv-search-fields-value',
61
				'value' => '[{"field":"search_all","input":"input_text"}]', // Default: Search Everything text box
62
			),
63
			'search_mode' => array(
64 39
				'type' => 'radio',
65
				'full_width' => true,
66 39
				'label' => esc_html__( 'Search Mode', 'gravityview' ),
67 39
				'desc' => __('Should search results match all search fields, or any?', 'gravityview'),
68 39
				'value' => 'any',
69 39
				'class' => 'hide-if-js',
70
				'options' => array(
71 39
					'any' => esc_html__( 'Match Any Fields', 'gravityview' ),
72 39
					'all' => esc_html__( 'Match All Fields', 'gravityview' ),
73
				),
74
			),
75
		);
76
77 39
		if ( ! $this->is_registered() ) {
78
			// frontend - filter entries
79
			add_filter( 'gravityview_fe_search_criteria', array( $this, 'filter_entries' ), 10, 3 );
80
81
			// frontend - add template path
82
			add_filter( 'gravityview_template_paths', array( $this, 'add_template_path' ) );
83
84
			// admin - add scripts - run at 1100 to make sure GravityView_Admin_Views::add_scripts_and_styles() runs first at 999
85
			add_action( 'admin_enqueue_scripts', array( $this, 'add_scripts_and_styles' ), 1100 );
86
			add_action( 'wp_enqueue_scripts', array( $this, 'register_scripts') );
87
			add_filter( 'gravityview_noconflict_scripts', array( $this, 'register_no_conflict' ) );
88
89
			// ajax - get the searchable fields
90
			add_action( 'wp_ajax_gv_searchable_fields', array( 'GravityView_Widget_Search', 'get_searchable_fields' ) );
91
92
			add_action( 'gravityview_search_widget_fields_after', array( $this, 'add_preview_inputs' ) );
93
94
			add_filter( 'gravityview/api/reserved_query_args', array( $this, 'add_reserved_args' ) );
95 39
		}
96
97
		parent::__construct( esc_html__( 'Search Bar', 'gravityview' ), null, $default_values, $settings );
98 39
99 39
		// calculate the search method (POST / GET)
100
		$this->set_search_method();
101
	}
102
103
	/**
104 6
	 * @return GravityView_Widget_Search
105 6
	 */
106
	public static function getInstance() {
107
		if ( empty( self::$instance ) ) {
108 6
			self::$instance = new GravityView_Widget_Search;
109
		}
110
		return self::$instance;
111
	}
112
113
	/**
114
	 * @since 2.10
115 39
	 *
116
	 * @param $args
117
	 *
118
	 * @return mixed
119
	 */
120
	public function add_reserved_args( $args ) {
121
122 39
		$args[] = 'gv_search';
123
		$args[] = 'gv_start';
124 39
		$args[] = 'gv_end';
125
		$args[] = 'gv_id';
126 39
		$args[] = 'gv_by';
127 39
		$args[] = 'mode';
128
129
		$get = (array) $_GET;
130
131
		// If the fields being searched as reserved; not to be considered user-passed variables
132
		foreach ( $get as $key => $value ) {
133
			if ( $key !== $this->convert_request_key_to_filter_key( $key ) ) {
134 6
				$args[] = $key;
135 6
			}
136
		}
137
138
		return $args;
139
	}
140
141
	/**
142
	 * Sets the search method to GET (default) or POST
143
	 * @since 1.16.4
144
	 */
145
	private function set_search_method() {
146
		/**
147
		 * @filter `gravityview/search/method` Modify the search form method (GET / POST)
148
		 * @since 1.16.4
149
		 * @param string $search_method Assign an input type according to the form field type. Defaults: `boolean`, `multi`, `select`, `date`, `text`
150
		 * @param string $field_type Gravity Forms field type (also the `name` parameter of GravityView_Field classes)
151
		 */
152
		$method = apply_filters( 'gravityview/search/method', $this->search_method );
153
154
		$method = strtolower( $method );
155
156
		$this->search_method = in_array( $method, array( 'get', 'post' ) ) ? $method : 'get';
157
	}
158
159
	/**
160
	 * Returns the search method
161
	 * @since 1.16.4
162
	 * @return string
163
	 */
164
	public function get_search_method() {
165
		return $this->search_method;
166
	}
167
168
	/**
169
	 * Get the input types available for different field types
170
	 *
171
	 * @since 1.17.5
172
	 *
173
	 * @return array [field type name] => (array|string) search bar input types
174
	 */
175
	public static function get_input_types_by_field_type() {
176
		/**
177
		 * Input Type groups
178
		 * @see admin-search-widget.js (getSelectInput)
179
		 */
180
		$input_types = array(
181
			'text' => array( 'input_text' ),
182
			'address' => array( 'input_text' ),
183
			'number' => array( 'input_text' ),
184
			'date' => array( 'date', 'date_range' ),
185
			'boolean' => array( 'single_checkbox' ),
186
			'select' => array( 'select', 'radio', 'link' ),
187
			'multi' => array( 'select', 'multiselect', 'radio', 'checkbox', 'link' ),
188
189
			// hybrids
190
			'created_by' => array( 'select', 'radio', 'checkbox', 'multiselect', 'link', 'input_text' ),
191
			'product'   => array( 'select', 'radio', 'link', 'input_text' ),
192
		);
193
194
		/**
195
		 * @filter `gravityview/search/input_types` Change the types of search fields available to a field type
196
		 * @see GravityView_Widget_Search::get_search_input_labels() for the available input types
197
		 * @param array $input_types Associative array: key is field `name`, value is array of GravityView input types (note: use `input_text` for `text`)
198
		 */
199
		$input_types = apply_filters( 'gravityview/search/input_types', $input_types );
200
201
		return $input_types;
202
	}
203
204
	/**
205
	 * Get labels for different types of search bar inputs
206
	 *
207
	 * @since 1.17.5
208
	 *
209
	 * @return array [input type] => input type label
210
	 */
211
	public static function get_search_input_labels() {
212
		/**
213
		 * Input Type labels l10n
214
		 * @see admin-search-widget.js (getSelectInput)
215
		 */
216
		$input_labels = array(
217
			'input_text' => esc_html__( 'Text', 'gravityview' ),
218
			'date' => esc_html__( 'Date', 'gravityview' ),
219
			'select' => esc_html__( 'Select', 'gravityview' ),
220
			'multiselect' => esc_html__( 'Select (multiple values)', 'gravityview' ),
221
			'radio' => esc_html__( 'Radio', 'gravityview' ),
222
			'checkbox' => esc_html__( 'Checkbox', 'gravityview' ),
223
			'single_checkbox' => esc_html__( 'Checkbox', 'gravityview' ),
224
			'link' => esc_html__( 'Links', 'gravityview' ),
225
			'date_range' => esc_html__( 'Date range', 'gravityview' ),
226
		);
227
228
		/**
229
		 * @filter `gravityview/search/input_types` Change the label of search field input types
230
		 * @param array $input_types Associative array: key is input type name, value is label
231
		 */
232
		$input_labels = apply_filters( 'gravityview/search/input_labels', $input_labels );
233
234
		return $input_labels;
235
	}
236
237
	public static function get_search_input_label( $input_type ) {
238
		$labels = self::get_search_input_labels();
239
240
		return \GV\Utils::get( $labels, $input_type, false );
241
	}
242
243
	/**
244
	 * Add script to Views edit screen (admin)
245
	 * @param  mixed $hook
246
	 */
247
	public function add_scripts_and_styles( $hook ) {
248
		global $pagenow;
249
250
		// Don't process any scripts below here if it's not a GravityView page or the widgets screen
251
		if ( ! gravityview()->request->is_admin( $hook, 'single' ) && ( 'widgets.php' !== $pagenow ) ) {
0 ignored issues
show
Unused Code introduced by
The call to Frontend_Request::is_admin() has too many arguments starting with $hook.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
Unused Code introduced by
The call to Request::is_admin() has too many arguments starting with $hook.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
252
			return;
253
		}
254
255
		$script_min = ( defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ) ? '' : '.min';
256
		$script_source = empty( $script_min ) ? '/source' : '';
257
258
		wp_enqueue_script( 'gravityview_searchwidget_admin', plugins_url( 'assets/js'.$script_source.'/admin-search-widget'.$script_min.'.js', __FILE__ ), array( 'jquery', 'gravityview_views_scripts' ), \GV\Plugin::$version );
259
260
		wp_localize_script( 'gravityview_searchwidget_admin', 'gvSearchVar', array(
261
			'nonce' => wp_create_nonce( 'gravityview_ajaxsearchwidget' ),
262
			'label_nofields' => esc_html__( 'No search fields configured yet.', 'gravityview' ),
263
			'label_addfield' => esc_html__( 'Add Search Field', 'gravityview' ),
264
			'label_label' => esc_html__( 'Label', 'gravityview' ),
265
			'label_searchfield' => esc_html__( 'Search Field', 'gravityview' ),
266
			'label_inputtype' => esc_html__( 'Input Type', 'gravityview' ),
267
			'label_ajaxerror' => esc_html__( 'There was an error loading searchable fields. Save the View or refresh the page to fix this issue.', 'gravityview' ),
268
			'input_labels' => json_encode( self::get_search_input_labels() ),
269
			'input_types' => json_encode( self::get_input_types_by_field_type() ),
270
		) );
271
272
	}
273
274
	/**
275
	 * Add admin script to the no-conflict scripts whitelist
276
	 * @param array $allowed Scripts allowed in no-conflict mode
277
	 * @return array Scripts allowed in no-conflict mode, plus the search widget script
278
	 */
279
	public function register_no_conflict( $allowed ) {
280
		$allowed[] = 'gravityview_searchwidget_admin';
281
		return $allowed;
282
	}
283
284
	/**
285
	 * Ajax
286
	 * Returns the form fields ( only the searchable ones )
287
	 *
288
	 * @return void
289
	 */
290
	public static function get_searchable_fields() {
291
292
		if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( $_POST['nonce'], 'gravityview_ajaxsearchwidget' ) ) {
293
			exit( '0' );
294
		}
295
296
		$form = '';
297
298
		// Fetch the form for the current View
299
		if ( ! empty( $_POST['view_id'] ) ) {
300
301
			$form = gravityview_get_form_id( $_POST['view_id'] );
302
303
		} elseif ( ! empty( $_POST['formid'] ) ) {
304
305
			$form = (int) $_POST['formid'];
306
307
		} elseif ( ! empty( $_POST['template_id'] ) && class_exists( 'GravityView_Ajax' ) ) {
308
309
			$form = GravityView_Ajax::pre_get_form_fields( $_POST['template_id'] );
310
311
		}
312
313
		// fetch form id assigned to the view
314
		$response = self::render_searchable_fields( $form );
315
316
		exit( $response );
317
	}
318
319
	/**
320
	 * Generates html for the available Search Fields dropdown
321
	 * @param  int $form_id
322
	 * @param  string $current (for future use)
323
	 * @return string
324
	 */
325
	public static function render_searchable_fields( $form_id = null, $current = '' ) {
326
327
		if ( is_null( $form_id ) ) {
328
			return '';
329
		}
330
331
		// start building output
332
333
		$output = '<select class="gv-search-fields">';
334
335
		$custom_fields = array(
336
			'search_all' => array(
337
				'text' => esc_html__( 'Search Everything', 'gravityview' ),
338
				'type' => 'text',
339
			),
340
			'entry_date' => array(
341
				'text' => esc_html__( 'Entry Date', 'gravityview' ),
342
				'type' => 'date',
343
			),
344
			'entry_id' => array(
345
				'text' => esc_html__( 'Entry ID', 'gravityview' ),
346
				'type' => 'text',
347
			),
348
			'created_by' => array(
349
				'text' => esc_html__( 'Entry Creator', 'gravityview' ),
350
				'type' => 'created_by',
351
			),
352
			'is_starred' => array(
353
				'text' => esc_html__( 'Is Starred', 'gravityview' ),
354
				'type' => 'boolean',
355
			),
356
		);
357
358
		if ( gravityview()->plugin->supports( \GV\Plugin::FEATURE_GFQUERY ) ) {
359
			$custom_fields['is_approved'] = array(
360
				'text' => esc_html__( 'Approval Status', 'gravityview' ),
361
				'type' => 'multi',
362
			);
363
		}
364
365
		foreach( $custom_fields as $custom_field_key => $custom_field ) {
366
			$output .= sprintf( '<option value="%s" %s data-inputtypes="%s" data-placeholder="%s">%s</option>', $custom_field_key, selected( $custom_field_key, $current, false ), $custom_field['type'], self::get_field_label( array('field' => $custom_field_key ) ), $custom_field['text'] );
367
		}
368
369
		// Get fields with sub-inputs and no parent
370
		$fields = gravityview_get_form_fields( $form_id, true, true );
371
372
		/**
373
		 * @filter `gravityview/search/searchable_fields` Modify the fields that are displayed as searchable in the Search Bar dropdown\n
374
		 * @since 1.17
375
		 * @see gravityview_get_form_fields() Used to fetch the fields
376
		 * @see GravityView_Widget_Search::get_search_input_types See this method to modify the type of input types allowed for a field
377
		 * @param array $fields Array of searchable fields, as fetched by gravityview_get_form_fields()
378
		 * @param  int $form_id
379
		 */
380
		$fields = apply_filters( 'gravityview/search/searchable_fields', $fields, $form_id );
381
382
		if ( ! empty( $fields ) ) {
383
384
			$blacklist_field_types = apply_filters( 'gravityview_blacklist_field_types', array( 'fileupload', 'post_image', 'post_id', 'section' ), null );
385
386
			foreach ( $fields as $id => $field ) {
387
388
				if ( in_array( $field['type'], $blacklist_field_types ) ) {
389
					continue;
390
				}
391
392
				$types = self::get_search_input_types( $id, $field['type'] );
393
394
				$output .= '<option value="'. $id .'" '. selected( $id, $current, false ).'data-inputtypes="'. esc_attr( $types ) .'">'. esc_html( $field['label'] ) .'</option>';
395
			}
396
		}
397
398
		$output .= '</select>';
399
400
		return $output;
401
402
	}
403
404
	/**
405
	 * Assign an input type according to the form field type
406
	 *
407
	 * @see admin-search-widget.js
408
	 *
409
	 * @param string|int|float $field_id Gravity Forms field ID
410
	 * @param string $field_type Gravity Forms field type (also the `name` parameter of GravityView_Field classes)
411
	 *
412
	 * @return string GV field search input type ('multi', 'boolean', 'select', 'date', 'text')
413
	 */
414
	public static function get_search_input_types( $field_id = '', $field_type = null ) {
415
416
		// @todo - This needs to be improved - many fields have . including products and addresses
417
		if ( false !== strpos( (string) $field_id, '.' ) && in_array( $field_type, array( 'checkbox' ) ) || in_array( $field_id, array( 'is_fulfilled' ) ) ) {
418
			$input_type = 'boolean'; // on/off checkbox
419
		} elseif ( in_array( $field_type, array( 'checkbox', 'post_category', 'multiselect' ) ) ) {
420
			$input_type = 'multi'; //multiselect
421
		} elseif ( in_array( $field_type, array( 'select', 'radio' ) ) ) {
422 4
			$input_type = 'select';
423
		} elseif ( in_array( $field_type, array( 'date' ) ) || in_array( $field_id, array( 'payment_date' ) ) ) {
424 4
			$input_type = 'date';
425
		} elseif ( in_array( $field_type, array( 'number' ) ) || in_array( $field_id, array( 'payment_amount' ) ) ) {
426
			$input_type = 'number';
427 4
		} elseif ( in_array( $field_type, array( 'product' ) ) ) {
428
			$input_type = 'product';
429
		} else {
430 4
			$input_type = 'text';
431
		}
432
433 4
		/**
434
		 * @filter `gravityview/extension/search/input_type` Modify the search form input type based on field type
435
		 * @since 1.2
436
		 * @since 1.19.2 Added $field_id parameter
437 4
		 * @param string $input_type Assign an input type according to the form field type. Defaults: `boolean`, `multi`, `select`, `date`, `text`
438
		 * @param string $field_type Gravity Forms field type (also the `name` parameter of GravityView_Field classes)
439
		 * @param string|int|float $field_id ID of the field being processed
440
		 */
441 4
		$input_type = apply_filters( 'gravityview/extension/search/input_type', $input_type, $field_type, $field_id );
442
443
		return $input_type;
444 4
	}
445 4
446 4
	/**
447 4
	 * Display hidden fields to add support for sites using Default permalink structure
448 4
	 *
449
	 * @since 1.8
450
	 * @return array Search fields, modified if not using permalinks
451
	 */
452
	public function add_no_permalink_fields( $search_fields, $object, $widget_args = array() ) {
0 ignored issues
show
Unused Code introduced by
The parameter $object 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...
453 4
		/** @global WP_Rewrite $wp_rewrite */
454
		global $wp_rewrite;
455
456
		// Support default permalink structure
457
		if ( false === $wp_rewrite->using_permalinks() ) {
458
459
			// By default, use current post.
460
			$post_id = 0;
461
462
			// We're in the WordPress Widget context, and an overriding post ID has been set.
463
			if ( ! empty( $widget_args['post_id'] ) ) {
464
				$post_id = absint( $widget_args['post_id'] );
465
			}
466
			// We're in the WordPress Widget context, and the base View ID should be used
467
			else if ( ! empty( $widget_args['view_id'] ) ) {
468
				$post_id = absint( $widget_args['view_id'] );
469
			}
470 39
471
			$args = gravityview_get_permalink_query_args( $post_id );
472
473
			// Add hidden fields to the search form
474
			foreach ( $args as $key => $value ) {
0 ignored issues
show
Bug introduced by
The expression $args of type array|null is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
475 39
				$search_fields[] = array(
476
					'name'  => $key,
477 39
					'input' => 'hidden',
478
					'value' => $value,
479
				);
480
			}
481
		}
482
483
		return $search_fields;
484 39
	}
485
486 39
	/**
487 39
	 * Get the fields that are searchable for a View
488
	 *
489
	 * @since 2.0
490
	 * @since 2.0.9 Added $with_full_field parameter
491
	 *
492
	 * @param \GV\View|null $view
493
	 * @param bool $with_full_field Return full field array, or just field ID? Default: false (just field ID)
494
	 *
495
	 * TODO: Move to \GV\View, perhaps? And return a Field_Collection
496
	 * TODO: Use in gravityview()->request->is_search() to calculate whether a valid search
497
	 *
498
	 * @return array If no View, returns empty array. Otherwise, returns array of fields configured in widgets and Search Bar for a View
499 39
	 */
500 33
	private function get_view_searchable_fields( $view, $with_full_field = false ) {
501 33
502 33
		/**
503 33
		 * Find all search widgets on the view and get the searchable fields settings.
504
		 */
505 33
		$searchable_fields = array();
506
507
		if ( ! $view ) {
508
			return $searchable_fields;
509
		}
510
511
		/**
512
		 * Include the sidebar Widgets.
513
		 */
514
		$widgets = (array) get_option( 'widget_gravityview_search', array() );
515
516
		foreach ( $widgets as $widget ) {
517
			if ( ! empty( $widget['view_id'] ) && $widget['view_id'] == $view->ID ) {
518 39
				if( $_fields = json_decode( $widget['search_fields'], true ) ) {
519
					foreach ( $_fields as $field ) {
520
						if ( empty( $field['form_id'] ) ) {
521
							$field['form_id'] = $view->form ? $view->form->ID : 0;
522
						}
523
						$searchable_fields[] = $with_full_field ? $field : $field['field'];
524
					}
525
				}
526
			}
527
		}
528
529
		foreach ( $view->widgets->by_id( $this->get_widget_id() )->all() as $widget ) {
530
			if( $_fields = json_decode( $widget->configuration->get( 'search_fields' ), true ) ) {
531
				foreach ( $_fields as $field ) {
532
					if ( empty( $field['form_id'] ) ) {
533 92
						$field['form_id'] = $view->form ? $view->form->ID : 0;
534 92
					}
535
					$searchable_fields[] = $with_full_field ? $field : $field['field'];
536
				}
537
			}
538
		}
539 72
540 72
		/**
541
		 * @filter `gravityview/search/searchable_fields/whitelist` Modifies the fields able to be searched using the Search Bar
542
		 * @since 2.5.1
543 91
		 *
544
		 * @param array $searchable_fields Array of GravityView-formatted fields or only the field ID? Example: [ '1.2', 'created_by' ]
545
		 * @param \GV\View $view Object of View being searched.
546 91
		 * @param bool $with_full_field Does $searchable_fields contain the full field array or just field ID? Default: false (just field ID)
547
		 */
548
		return apply_filters( 'gravityview/search/searchable_fields/whitelist', $searchable_fields, $view, $with_full_field );
549 91
	}
550
551 91
	/** --- Frontend --- */
552
553 91
	/**
554 64
	 * Calculate the search criteria to filter entries
555
	 * @param array $search_criteria The search criteria
556
	 * @param int $form_id The form ID
557 40
	 * @param array $args Some args
558
	 *
559 40
	 * @param bool $force_search_criteria Whether to suppress GF_Query filter, internally used in self::gf_query_filter
560
	 *
561
	 * @return array
562 40
	 */
563
	public function filter_entries( $search_criteria, $form_id = null, $args = array(), $force_search_criteria = false ) {
564 40
		if ( ! $force_search_criteria && gravityview()->plugin->supports( \GV\Plugin::FEATURE_GFQUERY ) ) {
565 40
			/**
566
			 * If GF_Query is available, we can construct custom conditions with nested
567
			 * booleans on the query, giving up the old ways of flat search_criteria field_filters.
568 40
			 */
569
			add_action( 'gravityview/view/query', array( $this, 'gf_query_filter' ), 10, 3 );
570 5
			return $search_criteria; // Return the original criteria, GF_Query modification kicks in later
571
		}
572
573
		if( 'post' === $this->search_method ) {
574
			$get = $_POST;
575
		} else {
576
			$get = $_GET;
577 5
		}
578
579 5
		$view = \GV\View::by_id( \GV\Utils::get( $args, 'id' ) );
580
581
		gravityview()->log->debug( 'Requested $_{method}: ', array( 'method' => $this->search_method, 'data' => $get ) );
582 5
583
		if ( empty( $get ) || ! is_array( $get ) ) {
584 5
			return $search_criteria;
585
		}
586
587
		$get = stripslashes_deep( $get );
588
589 1
		$get = gv_map_deep( $get, 'rawurldecode' );
590
591 1
		// Make sure array key is set up
592
		$search_criteria['field_filters'] = \GV\Utils::get( $search_criteria, 'field_filters', array() );
593
594 5
		$searchable_fields = $this->get_view_searchable_fields( $view );
595 5
		$searchable_field_objects = $this->get_view_searchable_fields( $view, true );
596 5
597 5
		/**
598 5
		 * @filter `gravityview/search-all-split-words` Search for each word separately or the whole phrase?
599
		 * @since 1.20.2
600
		 * @param bool $split_words True: split a phrase into words; False: search whole word only [Default: true]
601
		 */
602
		$split_words = apply_filters( 'gravityview/search-all-split-words', true );
603
604 40
		/**
605
		 * @filter `gravityview/search-trim-input` Remove leading/trailing whitespaces from search value
606
		 * @since 2.9.3
607
		 * @param bool $trim_search_value True: remove whitespace; False: keep as is [Default: true]
608 12
		 */
609 12
		$trim_search_value = apply_filters( 'gravityview/search-trim-input', true );
610 12
611
		// add free search
612
		if ( isset( $get['gv_search'] ) && '' !== $get['gv_search'] && in_array( 'search_all', $searchable_fields ) ) {
613
614 12
			$search_all_value = $trim_search_value ? trim( $get['gv_search'] ) : $get['gv_search'];
615 12
616 12
			if ( $split_words ) {
617
				// Search for a piece
618
				$words = explode( ' ', $search_all_value );
619
620 12
				$words = array_filter( $words );
621
622
			} else {
623
				// Replace multiple spaces with one space
624 12
				$search_all_value = preg_replace( '/\s+/ism', ' ', $search_all_value );
625 1
626 1
				$words = array( $search_all_value );
627
			}
628
629 12
			foreach ( $words as $word ) {
630
				$search_criteria['field_filters'][] = array(
631
					'key' => null, // The field ID to search
632
					'value' => $word, // The value to search
633
					'operator' => 'contains', // What to search in. Options: `is` or `contains`
634
				);
635
			}
636
		}
637
638
		// start date & end date
639
		if ( in_array( 'entry_date', $searchable_fields ) ) {
640
			/**
641
			 * Get and normalize the dates according to the input format.
642
			 */
643 12
			if ( $curr_start = ! empty( $get['gv_start'] ) ? $get['gv_start'] : '' ) {
644
				if( $curr_start_date = date_create_from_format( $this->get_datepicker_format( true ), $curr_start ) ) {
645
					$curr_start = $curr_start_date->format( 'Y-m-d' );
646
				}
647
			}
648 12
649 12
			if ( $curr_end = ! empty( $get['gv_start'] ) ? ( ! empty( $get['gv_end'] ) ? $get['gv_end'] : '' ) : '' ) {
650 12
				if( $curr_end_date = date_create_from_format( $this->get_datepicker_format( true ), $curr_end ) ) {
651
					$curr_end = $curr_end_date->format( 'Y-m-d' );
652
				}
653 12
			}
654
655 12
			if ( $view ) {
656 12
				/**
657 12
				 * Override start and end dates if View is limited to some already.
658 12
				 */
659
				if ( $start_date = $view->settings->get( 'start_date' ) ) {
660
					if ( $start_timestamp = strtotime( $curr_start ) ) {
661
						$curr_start = $start_timestamp < strtotime( $start_date ) ? $start_date : $curr_start;
662
					}
663
				}
664 40
				if ( $end_date = $view->settings->get( 'end_date' ) ) {
665 2
					if ( $end_timestamp = strtotime( $curr_end ) ) {
666 2
						$curr_end = $end_timestamp > strtotime( $end_date ) ? $end_date : $curr_end;
667 2
					}
668 2
				}
669
			}
670
671
			/**
672
			 * @filter `gravityview_date_created_adjust_timezone` Whether to adjust the timezone for entries. \n
673 40
			 * `date_created` is stored in UTC format. Convert search date into UTC (also used on templates/fields/date_created.php). \n
674 4
			 * This is for backward compatibility before \GF_Query started to automatically apply the timezone offset.
675 4
			 * @since 1.12
676 4
			 * @param[out,in] boolean $adjust_tz  Use timezone-adjusted datetime? If true, adjusts date based on blog's timezone setting. If false, uses UTC setting. Default is `false`.
677 4
			 * @param[in] string $context Where the filter is being called from. `search` in this case.
678
			 */
679
			$adjust_tz = apply_filters( 'gravityview_date_created_adjust_timezone', false, 'search' );
680
681
			/**
682 40
			 * Don't set $search_criteria['start_date'] if start_date is empty as it may lead to bad query results (GFAPI::get_entries)
683
			 */
684
			if ( ! empty( $curr_start ) ) {
685 40
				$curr_start = date( 'Y-m-d H:i:s', strtotime( $curr_start ) );
686
				$search_criteria['start_date'] = $adjust_tz ? get_gmt_from_date( $curr_start ) : $curr_start;
687 40
			}
688 27
689
			if ( ! empty( $curr_end ) ) {
690
				// Fast-forward 24 hour on the end time
691 17
				$curr_end = date( 'Y-m-d H:i:s', strtotime( $curr_end ) + DAY_IN_SECONDS );
692 1
				$search_criteria['end_date'] = $adjust_tz ? get_gmt_from_date( $curr_end ) : $curr_end;
693
				if ( strpos( $search_criteria['end_date'], '00:00:00' ) ) { // See https://github.com/gravityview/GravityView/issues/1056
694
					$search_criteria['end_date'] = date( 'Y-m-d H:i:s', strtotime( $search_criteria['end_date'] ) - 1 );
695 17
				}
696
			}
697 17
		}
698 3
699
		// search for a specific entry ID
700
		if ( ! empty( $get[ 'gv_id' ] ) && in_array( 'entry_id', $searchable_fields ) ) {
701 16
			$search_criteria['field_filters'][] = array(
702 7
				'key' => 'id',
703
				'value' => absint( $get[ 'gv_id' ] ),
704
				'operator' => $this->get_operator( $get, 'gv_id', array( '=' ), '=' ),
705 16
			);
706
		}
707
708
		// search for a specific Created_by ID
709
		if ( ! empty( $get[ 'gv_by' ] ) && in_array( 'created_by', $searchable_fields ) ) {
710
			$search_criteria['field_filters'][] = array(
711
				'key' => 'created_by',
712 16
				'value' => $get['gv_by'],
713 16
				'operator' => $this->get_operator( $get, 'gv_by', array( '=' ), '=' ),
714
			);
715
		}
716
717
		// Get search mode passed in URL
718
		$mode = isset( $get['mode'] ) && in_array( $get['mode'], array( 'any', 'all' ) ) ?  $get['mode'] : 'any';
719
720
		// get the other search filters
721
		foreach ( $get as $key => $value ) {
722 40
723
			if ( $trim_search_value ) {
724 40
				$value = is_array( $value ) ? array_map( 'trim', $value ) : trim( $value );
725
			}
726 40
727
			if ( 0 !== strpos( $key, 'filter_' ) || gv_empty( $value, false, false ) || ( is_array( $value ) && count( $value ) === 1 && gv_empty( $value[0], false, false ) ) ) {
728 40
				continue; // Not a filter, or empty
729
			}
730
731
			if ( strpos( $key, '|op' ) !== false ) {
732
				continue; // This is an operator
733
			}
734
735
			$filter_key = $this->convert_request_key_to_filter_key( $key );
736
737
			if ( ! $filter = $this->prepare_field_filter( $filter_key, $value, $view, $searchable_field_objects, $get ) ) {
738
				continue;
739
			}
740 71
741
			if ( ! isset( $filter['operator'] ) ) {
742
				$filter['operator'] = $this->get_operator( $get, $key, array( 'contains' ), 'contains' );
743
			}
744
745 71
			if ( isset( $filter[0]['value'] ) ) {
746
				$filter[0]['value'] = $trim_search_value ? trim( $filter[0]['value'] ) : $filter[0]['value'];
747
748
				$search_criteria['field_filters'] = array_merge( $search_criteria['field_filters'], $filter );
749
750 71
				// if date range type, set search mode to ALL
751 71
				if ( ! empty( $filter[0]['operator'] ) && in_array( $filter[0]['operator'], array( '>=', '<=', '>', '<' ) ) ) {
752 71
					$mode = 'all';
753
				}
754 71
			} elseif( !empty( $filter ) ) {
755
				$search_criteria['field_filters'][] = $filter;
756 71
			}
757 63
		}
758
759
		/**
760 20
		 * @filter `gravityview/search/mode` Set the Search Mode (`all` or `any`)
761 20
		 * @since 1.5.1
762 14
		 * @param[out,in] string $mode Search mode (`any` vs `all`)
763 14
		 */
764
		$search_criteria['field_filters']['mode'] = apply_filters( 'gravityview/search/mode', $mode );
765 14
766
		gravityview()->log->debug( 'Returned Search Criteria: ', array( 'data' => $search_criteria ) );
767 14
768 14
		unset( $get );
769 1
770
		return $search_criteria;
771
	}
772
773
	/**
774 20
	 * Filters the \GF_Query with advanced logic.
775 20
	 *
776
	 * Dropin for the legacy flat filters when \GF_Query is available.
777 20
	 *
778 20
	 * @param \GF_Query $query The current query object reference
779 20
	 * @param \GV\View $this The current view object
0 ignored issues
show
Bug introduced by
There is no parameter named $this. 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...
780 20
	 * @param \GV\Request $request The request object
781
	 */
782 20
	public function gf_query_filter( &$query, $view, $request ) {
0 ignored issues
show
Unused Code introduced by
The parameter $request 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...
783
		/**
784
		 * This is a shortcut to get all the needed search criteria.
785
		 * We feed these into an new GF_Query and tack them onto the current object.
786 13
		 */
787 2
		$search_criteria = $this->filter_entries( array(), null, array( 'id' => $view->ID ), true /** force search_criteria */ );
788 2
789
		/**
790 2
		 * Call any userland filters that they might have.
791 2
		 */
792 2
		remove_filter( 'gravityview_fe_search_criteria', array( $this, 'filter_entries' ), 10, 3 );
793
		$search_criteria = apply_filters( 'gravityview_fe_search_criteria', $search_criteria, $view->form->ID, $view->settings->as_atts() );
794
		add_filter( 'gravityview_fe_search_criteria', array( $this, 'filter_entries' ), 10, 3 );
795
796
		$query_class = $view->get_query_class();
797
798
		if ( empty( $search_criteria['field_filters'] ) ) {
799 2
			return;
800
		}
801
802 2
		$widgets = $view->widgets->by_id( $this->widget_id );
803
		if ( $widgets->count() ) {
804 2
			$widgets = $widgets->all();
805
			$widget  = $widgets[0];
806 2
807 2
			$search_fields = json_decode( $widget->configuration->get( 'search_fields' ), true );
808
809
			foreach ( (array) $search_fields as $search_field ) {
810
				if ( 'created_by' === $search_field['field'] && 'input_text' === $search_field['input'] ) {
811 13
					$created_by_text_mode = true;
812 1
				}
813 1
			}
814 1
		}
815
816
		$extra_conditions = array();
817
		$mode = 'any';
818 12
819
		foreach ( $search_criteria['field_filters'] as &$filter ) {
820
			if ( ! is_array( $filter ) ) {
821 12
				if ( in_array( strtolower( $filter ), array( 'any', 'all' ) ) ) {
822 3
					$mode = $filter;
823
				}
824
				continue;
825
			}
826 12
827 5
			// Construct a manual query for unapproved statuses
828
			if ( 'is_approved' === $filter['key'] && in_array( \GravityView_Entry_Approval_Status::UNAPPROVED, (array) $filter['value'] ) ) {
829
				$_tmp_query       = new $query_class( $view->form->ID, array(
830
					'field_filters' => array(
831
						array(
832
							'operator' => 'in',
833
							'key'      => 'is_approved',
834
							'value'    => (array) $filter['value'],
835
						),
836
						array(
837 12
							'operator' => 'is',
838
							'key'      => 'is_approved',
839
							'value'    => '',
840 20
						),
841 1
						'mode' => 'any'
842
					),
843 1
				) );
844 1
				$_tmp_query_parts = $_tmp_query->_introspect();
845
846
				$extra_conditions[] = $_tmp_query_parts['where'];
847 1
848 1
				$filter = false;
849
				continue;
850
			}
851 1
852 1
			// Construct manual query for text mode creator search
853 1
			if ( 'created_by' === $filter['key'] && ! empty( $created_by_text_mode ) ) {
854
				$extra_conditions[] = new GravityView_Widget_Search_Author_GF_Query_Condition( $filter, $view );
855
				$filter = false;
856 20
				continue;
857
			}
858 20
859 20
			// By default, we want searches to be wildcard for each field.
860 20
			$filter['operator'] = empty( $filter['operator'] ) ? 'contains' : $filter['operator'];
861 20
862
			// For multichoice, let's have an in (OR) search.
863
			if ( is_array( $filter['value'] ) ) {
864
				$filter['operator'] = 'in'; // @todo what about in contains (OR LIKE chains)?
865
			}
866
867
			// Default form with joins functionality
868
			if ( empty( $filter['form_id'] ) ) {
869
				$filter['form_id'] = $view->form ? $view->form->ID : 0;
870 12
			}
871 12
872 12
			/**
873
			 * @filter `gravityview_search_operator` Modify the search operator for the field (contains, is, isnot, etc)
874 12
			 * @param string $operator Existing search operator
875 1
			 * @param array $filter array with `key`, `value`, `operator`, `type` keys
876
			 * @since develop
877 11
			 * @param \GV\View $view The View we're operating on.
878 11
			 */
879
			$filter['operator'] = apply_filters( 'gravityview_search_operator', $filter['operator'], $filter, $view );
880 11
		}
881 2
882 2
		if ( ! empty( $search_criteria['start_date'] ) || ! empty( $search_criteria['end_date'] ) ) {
883 2
			$date_criteria = array();
884
885 2
			if ( isset( $search_criteria['start_date'] ) ) {
886
				$date_criteria['start_date'] = $search_criteria['start_date'];
887 2
			}
888 2
889 2
			if ( isset( $search_criteria['end_date'] ) ) {
890 2
				$date_criteria['end_date'] = $search_criteria['end_date'];
891
			}
892
893 2
			$_tmp_query         = new $query_class( $view->form->ID, $date_criteria );
894 2
			$_tmp_query_parts   = $_tmp_query->_introspect();
895 2
			$extra_conditions[] = $_tmp_query_parts['where'];
896 2
		}
897
898
		$search_conditions = array();
899
900
		if ( $filters = array_filter( $search_criteria['field_filters'] ) ) {
901 9
			foreach ( $filters as &$filter ) {
902 9
				if ( ! is_array( $filter ) ) {
903 9
					continue;
904 9
				}
905
906
				/**
907
				 * Parse the filter criteria to generate the needed
908
				 * WHERE condition. This is a trick to not write our own generation
909
				 * code by reusing what's inside GF_Query already as they
910 20
				 * take care of many small things like forcing numeric, etc.
911 12
				 */
912
				$_tmp_query       = new $query_class( $filter['form_id'], array( 'mode' => 'any', 'field_filters' => array( $filter ) ) );
913
				$_tmp_query_parts = $_tmp_query->_introspect();
914
				$search_condition = $_tmp_query_parts['where'];
915
916
				if ( empty( $filter['key'] ) && $search_condition->expressions ) {
917
					$search_conditions[] = $search_condition;
918 20
				} else {
919
					$left = $search_condition->left;
920
					$alias = $query->_alias( $left->field_id, $left->source, $left->is_entry_column() ? 't' : 'm' );
921
922
					if ( $view->joins && $left->field_id == GF_Query_Column::META ) {
923 20
						foreach ( $view->joins as $_join ) {
924 20
							$on = $_join->join_on;
925 20
							$join = $_join->join;
926
927
							$search_conditions[] = GF_Query_Condition::_or(
928
								// Join
929
								new GF_Query_Condition(
930
									new GF_Query_Column( GF_Query_Column::META, $join->ID, $query->_alias( GF_Query_Column::META, $join->ID, 'm' ) ),
931
									$search_condition->operator,
932
									$search_condition->right
933
								),
934
								// On
935
								new GF_Query_Condition(
936
									new GF_Query_Column( GF_Query_Column::META, $on->ID, $query->_alias( GF_Query_Column::META, $on->ID, 'm' ) ),
937
									$search_condition->operator,
938
									$search_condition->right
939
								)
940
							);
941 17
						}
942
					} else {
943 17
						$search_conditions[] = new GF_Query_Condition(
944
							new GF_Query_Column( $left->field_id, $left->source, $alias ),
945
							$search_condition->operator,
946 17
							$search_condition->right
947 15
						);
948
					}
949
				}
950 17
			}
951
952
			if ( $search_conditions ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $search_conditions of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
953
				$search_conditions = array( call_user_func_array( '\GF_Query_Condition::' . ( $mode == 'all' ? '_and' : '_or' ), $search_conditions ) );
954
			}
955
		}
956
957
		/**
958
		 * Grab the current clauses. We'll be combining them shortly.
959
		 */
960
		$query_parts = $query->_introspect();
961
962
		/**
963
		 * Combine the parts as a new WHERE clause.
964
		 */
965
		$where = call_user_func_array( '\GF_Query_Condition::_and', array_merge( array( $query_parts['where'] ), $search_conditions, $extra_conditions ) );
966
		$query->where( $where );
967
	}
968
969
	/**
970
	 * Convert $_GET/$_POST key to the field/meta ID
971 17
	 *
972 17
	 * Examples:
973 17
	 * - `filter_is_starred` => `is_starred`
974
	 * - `filter_1_2` => `1.2`
975 17
	 * - `filter_5` => `5`
976
	 *
977 17
	 * @since 2.0
978
	 *
979 1
	 * @param string $key $_GET/_$_POST search key
980
	 *
981 1
	 * @return string
982 1
	 */
983
	private function convert_request_key_to_filter_key( $key ) {
984
985
		$field_id = str_replace( 'filter_', '', $key );
986
987
		// calculates field_id, removing 'filter_' and for '_' for advanced fields ( like name or checkbox )
988 1
		if ( preg_match('/^[0-9_]+$/ism', $field_id ) ) {
989 1
			$field_id = str_replace( '_', '.', $field_id );
990 1
		}
991 1
992 1
		return $field_id;
993
	}
994
995
	/**
996 1
	 * Prepare the field filters to GFAPI
997
	 *
998
	 * The type post_category, multiselect and checkbox support multi-select search - each value needs to be separated in an independent filter so we could apply the ANY search mode.
999
	 *
1000
	 * Format searched values
1001 1
	 *
1002 1
	 * @param  string $filter_key ID of the field, or entry meta key
1003 1
	 * @param  string $value $_GET/$_POST search value
1004 1
	 * @param  \GV\View $view The view we're looking at
1005 1
	 * @param array[] $searchable_fields The searchable fields as configured by the widget.
1006
	 * @param string[] $get The $_GET/$_POST array.
1007
	 *
1008
	 * @since develop Added 5th $get parameter for operator overrides.
1009 1
	 * @todo Set function as private.
1010 1
	 *
1011
	 * @return array|false 1 or 2 deph levels, false if not allowed
1012
	 */
1013 17
	public function prepare_field_filter( $filter_key, $value, $view, $searchable_fields, $get = array() ) {
1014 17
		$key = $filter_key;
1015 17
		$filter_key = explode( ':', $filter_key ); // field_id, form_id
1016 2
1017
		$form = null;
1018
1019
		if ( count( $filter_key ) > 1 ) {
1020 16
			// form is specified
1021
			list( $field_id, $form_id ) = $filter_key;
1022 16
1023
			if ( $forms = \GV\View::get_joined_forms( $view->ID ) ) {
1024
				if ( ! $form = \GV\GF_Form::by_id( $form_id ) ) {
1025
					return false;
1026 16
				}
1027
			}
1028 16
1029
			// form is allowed
1030
			$found = false;
1031
			foreach ( $forms as $form ) {
1032
				if ( $form->ID == $form_id ) {
1033
					$found = true;
1034 16
					break;
1035 16
				}
1036 16
			}
1037
1038
			if ( ! $found ) {
1039 16
				return false;
1040
			}
1041 16
1042 16
			// form is in searchable fields
1043 1
			$found = false;
1044 1
			foreach ( $searchable_fields as $field ) {
1045
				if ( $field_id == $field['field'] && $form->ID == $field['form_id'] ) {
1046 15
					$found = true;
1047
					break;
1048
				}
1049
			}
1050
1051
			if ( ! $found ) {
1052
				return false;
1053
			}
1054
		} else {
1055
			$field_id = reset( $filter_key );
1056
			$searchable_fields = wp_list_pluck( $searchable_fields, 'field' );
1057
			if ( ! in_array( 'search_all', $searchable_fields ) && ! in_array( $field_id, $searchable_fields ) ) {
1058
				return false;
1059
			}
1060
		}
1061
1062
		if ( ! $form ) {
1063
			// fallback
1064
			$form = $view->form;
1065
		}
1066 15
1067
		// get form field array
1068
		$form_field = is_numeric( $field_id ) ? \GV\GF_Field::by_id( $form, $field_id ) : \GV\Internal_Field::by_id( $field_id );
1069
1070
		if ( ! $form_field ) {
1071
			return false;
1072
		}
1073
1074
		// default filter array
1075
		$filter = array(
1076
			'key'   => $field_id,
1077
			'value' => $value,
1078
			'form_id' => $form->ID,
1079
		);
1080
1081 15
		switch ( $form_field->type ) {
1082
1083
			case 'select':
1084
			case 'radio':
1085
				$filter['operator'] = $this->get_operator( $get, $key, array( 'is' ), 'is' );
1086
				break;
1087
1088
			case 'post_category':
1089
1090
				if ( ! is_array( $value ) ) {
1091
					$value = array( $value );
1092
				}
1093
1094
				// Reset filter variable
1095
				$filter = array();
1096
1097
				foreach ( $value as $val ) {
1098
					$cat = get_term( $val, 'category' );
1099
					$filter[] = array(
1100
						'key'      => $field_id,
1101
						'value'    => esc_attr( $cat->name ) . ':' . $val,
1102
						'operator' => $this->get_operator( $get, $key, array( 'is' ), 'is' ),
1103
					);
1104
				}
1105
1106
				break;
1107 15
1108 15
			case 'multiselect':
1109
1110 1
				if ( ! is_array( $value ) ) {
1111
					break;
1112
				}
1113
1114
				// Reset filter variable
1115
				$filter = array();
1116
1117
				foreach ( $value as $val ) {
1118
					$filter[] = array( 'key' => $field_id, 'value' => $val );
1119
				}
1120
1121
				break;
1122
1123
			case 'checkbox':
1124
				// convert checkbox on/off into the correct search filter
1125
				if ( false !== strpos( $field_id, '.' ) && ! empty( $form_field->inputs ) && ! empty( $form_field->choices ) ) {
1126
					foreach ( $form_field->inputs as $k => $input ) {
1127
						if ( $input['id'] == $field_id ) {
1128 1
							$filter['value'] = $form_field->choices[ $k ]['value'];
1129
							$filter['operator'] = $this->get_operator( $get, $key, array( 'is' ), 'is' );
1130 1
							break;
1131
						}
1132 1
					}
1133
				} elseif ( is_array( $value ) ) {
1134 1
1135
					// Reset filter variable
1136
					$filter = array();
1137
1138
					foreach ( $value as $val ) {
1139 1
						$filter[] = array(
1140 1
							'key'      => $field_id,
1141
							'value'    => $val,
1142
							'operator' => $this->get_operator( $get, $key, array( 'is' ), 'is' ),
1143
						);
1144
					}
1145
				}
1146
1147
				break;
1148
1149
			case 'name':
1150
			case 'address':
1151 1
1152
				if ( false === strpos( $field_id, '.' ) ) {
1153 15
1154
					$words = explode( ' ', $value );
1155 8
1156
					$filters = array();
1157 8
					foreach ( $words as $word ) {
1158
						if ( ! empty( $word ) && strlen( $word ) > 1 ) {
1159
							// Keep the same key for each filter
1160
							$filter['value'] = $word;
1161
							// Add a search for the value
1162
							$filters[] = $filter;
1163
						}
1164
					}
1165
1166
					$filter = $filters;
1167
				}
1168
1169
				// State/Province should be exact matches
1170
				if ( 'address' === $form_field->field->type ) {
1171
1172
					$searchable_fields = $this->get_view_searchable_fields( $view, true );
1173
1174
					foreach ( $searchable_fields as $searchable_field ) {
1175
1176
						if( $form_field->ID !== $searchable_field['field'] ) {
1177
							continue;
1178
						}
1179
1180
						// Only exact-match dropdowns, not text search
1181
						if( in_array( $searchable_field['input'], array( 'text', 'search' ), true ) ) {
1182
							continue;
1183
						}
1184
1185 8
						$input_id = gravityview_get_input_id_from_id( $form_field->ID );
1186 8
1187 8
						if ( 4 === $input_id ) {
1188
							$filter['operator'] = $this->get_operator( $get, $key, array( 'is' ), 'is' );
1189
						};
1190 8
					}
1191
				}
1192
1193
				break;
1194
1195 16
			case 'payment_date':
1196
			case 'date':
1197
1198
				$date_format = $this->get_datepicker_format( true );
1199
1200
				if ( is_array( $value ) ) {
1201
1202
					// Reset filter variable
1203
					$filter = array();
1204
1205
					foreach ( $value as $k => $date ) {
1206
						if ( empty( $date ) ) {
1207
							continue;
1208
						}
1209
						$operator = 'start' === $k ? '>=' : '<=';
1210
1211
						/**
1212
						 * @hack
1213
						 * @since 1.16.3
1214
						 * Safeguard until GF implements '<=' operator
1215
						 */
1216
						if( !GFFormsModel::is_valid_operator( $operator ) && $operator === '<=' ) {
1217
							$operator = '<';
1218
							$date = date( 'Y-m-d', strtotime( self::get_formatted_date( $date, 'Y-m-d', $date_format ) . ' +1 day' ) );
1219
						}
1220
1221
						$filter[] = array(
1222
							'key'      => $field_id,
1223
							'value'    => self::get_formatted_date( $date, 'Y-m-d', $date_format ),
1224
							'operator' => $this->get_operator( $get, $key, array( $operator ), $operator ),
1225
						);
1226
					}
1227
				} else {
1228
					$date = $value;
1229
					$filter['value'] = self::get_formatted_date( $date, 'Y-m-d', $date_format );
1230
					$filter['operator'] = $this->get_operator( $get, $key, array( 'is' ), 'is' );
1231
				}
1232
1233
				if ('payment_date' === $key) {
1234
					$filter['operator'] = 'contains';
1235
				}
1236 8
1237
				break;
1238 8
		} // switch field type
1239
1240 8
		return $filter;
1241
	}
1242
1243
	/**
1244 8
	 * Get the Field Format form GravityForms
1245
	 *
1246
	 * @param GF_Field_Date $field The field object
1247
	 * @since 1.10
1248
	 *
1249
	 * @return string Format of the date in the database
1250
	 */
1251
	public static function get_date_field_format( GF_Field_Date $field ) {
1252 1
		$format = 'm/d/Y';
1253
		$datepicker = array(
1254
			'mdy' => 'm/d/Y',
1255 1
			'dmy' => 'd/m/Y',
1256
			'dmy_dash' => 'd-m-Y',
1257 1
			'dmy_dot' => 'd.m.Y',
1258
			'ymd_slash' => 'Y/m/d',
1259
			'ymd_dash' => 'Y-m-d',
1260
			'ymd_dot' => 'Y.m.d',
1261
		);
1262
1263
		if ( ! empty( $field->dateFormat ) && isset( $datepicker[ $field->dateFormat ] ) ){
1264
			$format = $datepicker[ $field->dateFormat ];
1265
		}
1266
1267
		return $format;
1268
	}
1269 4
1270
	/**
1271 4
	 * Format a date value
1272
	 *
1273 4
	 * @param string $value Date value input
1274 4
	 * @param string $format Wanted formatted date
1275
	 *
1276
	 * @since 2.1.2
1277
	 * @param string $value_format The value format. Default: Y-m-d
1278
	 *
1279
	 * @return string
1280 4
	 */
1281
	public static function get_formatted_date( $value = '', $format = 'Y-m-d', $value_format = 'Y-m-d' ) {
1282
1283
		$date = date_create_from_format( $value_format, $value );
1284
1285
		if ( empty( $date ) ) {
1286
			gravityview()->log->debug( 'Date format not valid: {value}', array( 'value' => $value ) );
1287
			return '';
1288
		}
1289
		return $date->format( $format );
1290
	}
1291 4
1292
1293 4
	/**
1294
	 * Include this extension templates path
1295 4
	 * @param array $file_paths List of template paths ordered
1296
	 */
1297
	public function add_template_path( $file_paths ) {
1298
1299
		// Index 100 is the default GravityView template path.
1300 4
		$file_paths[102] = self::$file . 'templates/';
1301
1302
		return $file_paths;
1303 4
	}
1304
1305 4
	/**
1306
	 * Check whether the configured search fields have a date field
1307
	 *
1308
	 * @since 1.17.5
1309
	 *
1310
	 * @param array $search_fields
1311 4
	 *
1312
	 * @return bool True: has a `date` or `date_range` field
1313 4
	 */
1314
	private function has_date_field( $search_fields ) {
1315 4
1316
		$has_date = false;
1317 4
1318
		foreach ( $search_fields as $k => $field ) {
1319 4
			if ( in_array( $field['input'], array( 'date', 'date_range', 'entry_date' ) ) ) {
1320 4
				$has_date = true;
1321 4
				break;
1322 4
			}
1323 4
		}
1324
1325
		return $has_date;
1326
	}
1327
1328
	/**
1329
	 * Renders the Search Widget
1330
	 * @param array $widget_args
1331
	 * @param string $content
1332
	 * @param string|\GV\Template_Context $context
1333
	 *
1334
	 * @return void
1335
	 */
1336
	public function render_frontend( $widget_args, $content = '', $context = '' ) {
1337
1338
		$gravityview_view = GravityView_View::getInstance();
1339
1340
		if ( empty( $gravityview_view ) ) {
1341
			gravityview()->log->debug( '$gravityview_view not instantiated yet.' );
1342
			return;
1343
		}
1344
1345
		$view = \GV\View::by_id( $gravityview_view->view_id );
1346
1347
		// get configured search fields
1348
		$search_fields = ! empty( $widget_args['search_fields'] ) ? json_decode( $widget_args['search_fields'], true ) : '';
1349
1350
		if ( empty( $search_fields ) || ! is_array( $search_fields ) ) {
1351
			gravityview()->log->debug( 'No search fields configured for widget:', array( 'data' => $widget_args ) );
1352
			return;
1353
		}
1354 4
1355
		// prepare fields
1356
		foreach ( $search_fields as $k => $field ) {
1357 4
1358
			$updated_field = $field;
1359
1360
			$updated_field = $this->get_search_filter_details( $updated_field, $context );
1361
1362
			switch ( $field['field'] ) {
1363
1364
				case 'search_all':
1365
					$updated_field['key'] = 'search_all';
1366
					$updated_field['input'] = 'search_all';
1367 4
					$updated_field['value'] = $this->rgget_or_rgpost( 'gv_search' );
1368
					break;
1369 4
1370
				case 'entry_date':
1371 4
					$updated_field['key'] = 'entry_date';
1372
					$updated_field['input'] = 'entry_date';
1373
					$updated_field['value'] = array(
1374 4
						'start' => $this->rgget_or_rgpost( 'gv_start' ),
1375
						'end' => $this->rgget_or_rgpost( 'gv_end' ),
1376 4
					);
1377
					break;
1378 4
1379
				case 'entry_id':
1380 4
					$updated_field['key'] = 'entry_id';
1381
					$updated_field['input'] = 'entry_id';
1382 4
					$updated_field['value'] = $this->rgget_or_rgpost( 'gv_id' );
1383
					break;
1384
1385
				case 'created_by':
1386
					$updated_field['key'] = 'created_by';
1387 4
					$updated_field['name'] = 'gv_by';
1388
					$updated_field['value'] = $this->rgget_or_rgpost( 'gv_by' );
1389 4
					$updated_field['choices'] = self::get_created_by_choices( $view );
1390 4
					break;
1391
1392
				case 'is_approved':
1393
					$updated_field['key'] = 'is_approved';
1394
					$updated_field['value'] = $this->rgget_or_rgpost( 'filter_is_approved' );
1395
					$updated_field['choices'] = self::get_is_approved_choices();
1396
					break;
1397
			}
1398
1399 4
			$search_fields[ $k ] = $updated_field;
1400 4
		}
1401
1402 4
		gravityview()->log->debug( 'Calculated Search Fields: ', array( 'data' => $search_fields ) );
1403
1404 4
		/**
1405
		 * @filter `gravityview_widget_search_filters` Modify what fields are shown. The order of the fields in the $search_filters array controls the order as displayed in the search bar widget.
1406
		 * @param array $search_fields Array of search filters with `key`, `label`, `value`, `type`, `choices` keys
1407
		 * @param GravityView_Widget_Search $this Current widget object
1408
		 * @param array $widget_args Args passed to this method. {@since 1.8}
1409
		 * @param \GV\Template_Context $context {@since 2.0}
1410
		 * @type array
1411
		 */
1412 4
		$gravityview_view->search_fields = apply_filters( 'gravityview_widget_search_filters', $search_fields, $this, $widget_args, $context );
0 ignored issues
show
Bug introduced by
The property search_fields does not seem to exist. Did you mean fields?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
1413
1414
		$gravityview_view->permalink_fields = $this->add_no_permalink_fields( array(), $this, $widget_args );
0 ignored issues
show
Bug introduced by
The property permalink_fields does not seem to exist. Did you mean fields?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
1415 4
1416
		$gravityview_view->search_layout = ! empty( $widget_args['search_layout'] ) ? $widget_args['search_layout'] : 'horizontal';
0 ignored issues
show
Bug introduced by
The property search_layout does not seem to exist in GravityView_View.

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
1417 4
1418
		/** @since 1.14 */
1419
		$gravityview_view->search_mode = ! empty( $widget_args['search_mode'] ) ? $widget_args['search_mode'] : 'any';
0 ignored issues
show
Bug introduced by
The property search_mode does not seem to exist in GravityView_View.

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
1420
1421
		$custom_class = ! empty( $widget_args['custom_class'] ) ? $widget_args['custom_class'] : '';
1422
1423
		$gravityview_view->search_class = self::get_search_class( $custom_class );
0 ignored issues
show
Bug introduced by
The property search_class does not seem to exist in GravityView_View.

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
1424
1425
		$gravityview_view->search_clear = ! empty( $widget_args['search_clear'] ) ? $widget_args['search_clear'] : false;
0 ignored issues
show
Bug introduced by
The property search_clear does not seem to exist in GravityView_View.

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
1426
1427 4
		if ( $this->has_date_field( $search_fields ) ) {
1428 4
			// enqueue datepicker stuff only if needed!
1429
			$this->enqueue_datepicker();
1430 4
		}
1431
1432 4
		$this->maybe_enqueue_flexibility();
1433
1434
		$gravityview_view->render( 'widget', 'search', false );
1435
	}
1436
1437
	/**
1438
	 * Get the search class for a search form
1439
	 *
1440
	 * @since 1.5.4
1441 4
	 *
1442
	 * @return string Sanitized CSS class for the search form
1443
	 */
1444
	public static function get_search_class( $custom_class = '' ) {
1445
		$gravityview_view = GravityView_View::getInstance();
1446
1447
		$search_class = 'gv-search-'.$gravityview_view->search_layout;
0 ignored issues
show
Documentation introduced by
The property search_layout does not exist on object<GravityView_View>. Since you implemented __get, maybe consider adding a @property annotation.

Since your code implements the magic getter _get, this function will be called for any read access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

If the property has read access only, you can use the @property-read annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
1448
1449
		if ( ! empty( $custom_class )  ) {
1450 4
			$search_class .= ' '.$custom_class;
1451
		}
1452 4
1453
		/**
1454 4
		 * @filter `gravityview_search_class` Modify the CSS class for the search form
1455
		 * @param string $search_class The CSS class for the search form
1456 4
		 */
1457
		$search_class = apply_filters( 'gravityview_search_class', $search_class );
1458 4
1459 4
		// Is there an active search being performed? Used by fe-views.js
1460 4
		$search_class .= GravityView_frontend::getInstance()->isSearch() ? ' gv-is-search' : '';
1461 4
1462
		return gravityview_sanitize_html_class( $search_class );
1463
	}
1464
1465
1466
	/**
1467
	 * Calculate the search form action
1468
	 * @since 1.6
1469
	 *
1470
	 * @return string
1471
	 */
1472
	public static function get_search_form_action() {
1473
		$gravityview_view = GravityView_View::getInstance();
1474
1475
		$post_id = $gravityview_view->getPostId() ? $gravityview_view->getPostId() : $gravityview_view->getViewId();
1476
1477
		$url = add_query_arg( array(), get_permalink( $post_id ) );
1478
1479
		/**
1480
		 * @filter `gravityview/widget/search/form/action` Override the search URL.
1481
		 * @param[in,out] string $action Where the form submits to.
1482
		 *
1483
		 * Further parameters will be added once adhoc context is added.
1484
		 * Use gravityview()->request until then.
1485
		 */
1486
		return apply_filters( 'gravityview/widget/search/form/action', $url );
1487
	}
1488
1489
	/**
1490
	 * Get the label for a search form field
1491
	 * @param  array $field      Field setting as sent by the GV configuration - has `field`, `input` (input type), and `label` keys
1492
	 * @param  array $form_field Form field data, as fetched by `gravityview_get_field()`
1493
	 * @return string             Label for the search form
1494 4
	 */
1495
	private static function get_field_label( $field, $form_field = array() ) {
1496 4
1497
		$label = \GV\Utils::_GET( 'label', \GV\Utils::get( $field, 'label' ) );
1498
1499
		if ( ! $label ) {
1500
1501
			$label = isset( $form_field['label'] ) ? $form_field['label'] : '';
1502
1503
			switch( $field['field'] ) {
1504
				case 'search_all':
1505
					$label = __( 'Search Entries:', 'gravityview' );
1506
					break;
1507 4
				case 'entry_date':
1508
					$label = __( 'Filter by date:', 'gravityview' );
1509 4
					break;
1510
				case 'entry_id':
1511 4
					$label = __( 'Entry ID:', 'gravityview' );
1512
					break;
1513
				default:
1514 4
					// If this is a field input, not a field
1515
					if ( strpos( $field['field'], '.' ) > 0 && ! empty( $form_field['inputs'] ) ) {
1516
1517 4
						// Get the label for the field in question, which returns an array
1518
						$items = wp_list_filter( $form_field['inputs'], array( 'id' => $field['field'] ) );
1519
1520 4
						// Get the item with the `label` key
1521
						$values = wp_list_pluck( $items, 'label' );
1522 4
1523
						// There will only one item in the array, but this is easier
1524
						foreach ( $values as $value ) {
1525 4
							$label = $value;
1526 4
							break;
1527 4
						}
1528 4
					}
1529 4
			}
1530 4
		}
1531
1532
		/**
1533
		 * @filter `gravityview_search_field_label` Modify the label for a search field. Supports returning HTML
1534 4
		 * @since 1.17.3 Added $field parameter
1535
		 * @param[in,out] string $label Existing label text, sanitized.
1536 4
		 * @param[in] array $form_field Gravity Forms field array, as returned by `GFFormsModel::get_field()`
1537
		 * @param[in] array $field Field setting as sent by the GV configuration - has `field`, `input` (input type), and `label` keys
1538
		 */
1539
		$label = apply_filters( 'gravityview_search_field_label', esc_attr( $label ), $form_field, $field );
1540 4
1541
		return $label;
1542
	}
1543
1544 4
	/**
1545
	 * Prepare search fields to frontend render with other details (label, field type, searched values)
1546
	 *
1547
	 * @param array $field
1548
	 * @param \GV\Context $context
1549
	 *
1550
	 * @return array
1551
	 */
1552
	private function get_search_filter_details( $field, $context ) {
1553
1554
		$gravityview_view = GravityView_View::getInstance();
1555
1556
		$form = $gravityview_view->getForm();
1557
1558
		// for advanced field ids (eg, first name / last name )
1559
		$name = 'filter_' . str_replace( '.', '_', $field['field'] );
1560
1561
		// get searched value from $_GET/$_POST (string or array)
1562
		$value = $this->rgget_or_rgpost( $name );
1563 4
1564
		// get form field details
1565 4
		$form_field = gravityview_get_field( $form, $field['field'] );
1566
1567
		$form_field_type = \GV\Utils::get( $form_field, 'type' );
1568
1569
		$filter = array(
1570
			'key' => \GV\Utils::get( $field, 'field' ),
1571
			'name' => $name,
1572
			'label' => self::get_field_label( $field, $form_field ),
1573
			'input' => \GV\Utils::get( $field, 'input' ),
1574
			'value' => $value,
1575
			'type' => $form_field_type,
1576
		);
1577
1578
		// collect choices
1579
		if ( 'post_category' === $form_field_type && ! empty( $form_field['displayAllCategories'] ) && empty( $form_field['choices'] ) ) {
1580
			$filter['choices'] = gravityview_get_terms_choices();
1581
		} elseif ( ! empty( $form_field['choices'] ) ) {
1582
			$filter['choices'] = $form_field['choices'];
1583
		}
1584
1585
		if ( 'date_range' === $field['input'] && empty( $value ) ) {
1586
			$filter['value'] = array( 'start' => '', 'end' => '' );
1587
		}
1588
1589
		if ( ! empty( $filter['choices'] ) ) {
1590
			/**
1591
			 * @filter `gravityview/search/sieve_choices` Only output used choices for this field.
1592
			 * @param[in,out] bool Yes or no.
1593
			 * @param array $field The field configuration.
1594
			 * @param \GV\Context The context.
1595
			 */
1596
			if ( apply_filters( 'gravityview/search/sieve_choices', false, $field, $context ) ) {
1597
				$filter['choices'] = $this->sieve_filter_choices( $filter, $context );
1598
			}
1599
		}
1600
1601
		/**
1602
		 * @filter `gravityview/search/filter_details` Filter the output filter details for the Search widget.
1603
		 * @param[in,out] array $filter The filter details
1604
		 * @param array $field The search field configuration
1605
		 * @param \GV\Context The context
1606
		 * @since develop
1607
		 */
1608
		$filter = apply_filters( 'gravityview/search/filter_details', $filter, $field, $context );
1609
1610
		return $filter;
1611
1612
	}
1613
1614
	/**
1615
	 * Sieve filter choices to only ones that are used.
1616
	 *
1617
	 * @param array $filter The filter configuration.
1618
	 * @param \GV\Context $context The context
1619
	 *
1620
	 * @since develop
1621
	 * @internal
1622
	 *
1623
	 * @return array The filter choices.
1624
	 */
1625
	private function sieve_filter_choices( $filter, $context ) {
1626
		if ( empty( $filter['key'] ) || empty( $filter['choices'] ) ) {
1627
			return $filter; // @todo Populate plugins might give us empty choices
1628
		}
1629
1630
		if ( ! is_numeric( $filter['key'] ) ) {
1631
			return $filter;
1632
		}
1633
1634
		$form_id = $context->view->form->ID; // @todo Support multiple forms (joins)
0 ignored issues
show
Bug introduced by
The property view does not seem to exist in GV\Context.

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
1635
1636
		global $wpdb;
1637
1638
		$table = GFFormsModel::get_entry_meta_table_name();
1639
1640
		$key_like = $wpdb->esc_like( $filter['key'] ) . '.%';
1641
1642
		switch ( \GV\Utils::get( $filter, 'type' ) ):
1643
			case 'post_category':
1644
				$choices = $wpdb->get_col( $wpdb->prepare(
1645
					"SELECT DISTINCT SUBSTRING_INDEX(meta_value, ':', 1) FROM $table WHERE (meta_key LIKE %s OR meta_key = %d) AND form_id = %d",
1646
					$key_like, $filter['key'], $form_id
1647
				) );
1648
				break;
1649
			default:
1650
				$choices = $wpdb->get_col( $wpdb->prepare(
1651
					"SELECT DISTINCT meta_value FROM $table WHERE (meta_key LIKE %s OR meta_key = %d) AND form_id = %d",
1652
					$key_like, $filter['key'], $form_id
1653
				) );
1654
1655
				if ( ( $field = gravityview_get_field( $form_id, $filter['key'] ) ) && 'json' === $field->storageType ) {
1656
					$choices = array_map( 'json_decode', $choices );
1657
					$_choices_array = array();
1658
					foreach ( $choices as $choice ) {
1659
						if ( is_array( $choice ) ) {
1660
							$_choices_array = array_merge( $_choices_array, $choice );
1661
						} else {
1662
							$_choices_array []= $choice;
1663
						}
1664
					}
1665
					$choices = array_unique( $_choices_array );
1666
				}
1667
1668
				break;
1669
		endswitch;
1670
1671
		$filter_choices = array();
1672
		foreach ( $filter['choices'] as $choice ) {
1673
			if ( in_array( $choice['text'], $choices, true ) || in_array( $choice['value'], $choices, true ) ) {
1674
				$filter_choices[] = $choice;
1675
			}
1676
		}
1677
1678
		return $filter_choices;
1679
	}
1680
1681
	/**
1682
	 * Calculate the search choices for the users
1683
	 *
1684
	 * @param \GV\View $view The view
1685
	 * @since develop
1686
	 *
1687
	 * @since 1.8
1688
	 *
1689
	 * @return array Array of user choices (value = ID, text = display name)
1690
	 */
1691
	private static function get_created_by_choices( $view ) {
1692
1693
		/**
1694
		 * filter gravityview/get_users/search_widget
1695
		 * @see \GVCommon::get_users
1696
		 */
1697 4
		$users = GVCommon::get_users( 'search_widget', array( 'fields' => array( 'ID', 'display_name' ) ) );
1698 4
1699
		$choices = array();
1700 4
		foreach ( $users as $user ) {
1701
			/**
1702
			 * @filter `gravityview/search/created_by/text` Filter the display text in created by search choices
1703
			 * @since develop
1704
			 * @param string[in,out] The text. Default: $user->display_name
1705
			 * @param \WP_User $user The user.
1706
			 * @param \GV\View $view The view.
1707 4
			 */
1708
			$text = apply_filters( 'gravityview/search/created_by/text', $user->display_name, $user, $view );
1709
			$choices[] = array(
1710
				'value' => $user->ID,
1711
				'text' => $text,
1712
			);
1713
		}
1714
1715
		return $choices;
1716
	}
1717
1718 4
	/**
1719 4
	 * Calculate the search checkbox choices for approval status
1720
	 *
1721 4
	 * @since develop
1722
	 *
1723 4
	 * @return array Array of approval status choices (value = status, text = display name)
1724
	 */
1725 4
	private static function get_is_approved_choices() {
1726
1727 4
		$choices = array();
1728
		foreach ( GravityView_Entry_Approval_Status::get_all() as $status ) {
1729
			$choices[] = array(
1730
				'value' => $status['value'],
1731
				'text' => $status['label'],
1732
			);
1733
		}
1734
1735
		return $choices;
1736
	}
1737
1738
	/**
1739
	 * Output the Clear Search Results button
1740
	 * @since 1.5.4
1741
	 */
1742
	public static function the_clear_search_button() {
1743
		$gravityview_view = GravityView_View::getInstance();
1744
1745
		if ( $gravityview_view->search_clear ) {
0 ignored issues
show
Documentation introduced by
The property search_clear does not exist on object<GravityView_View>. Since you implemented __get, maybe consider adding a @property annotation.

Since your code implements the magic getter _get, this function will be called for any read access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

If the property has read access only, you can use the @property-read annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
1746
1747
			$url = strtok( add_query_arg( array() ), '?' );
1748
1749
			echo gravityview_get_link( $url, esc_html__( 'Clear', 'gravityview' ), 'class=button gv-search-clear' );
1750
1751
		}
1752
	}
1753
1754
	/**
1755
	 * Based on the search method, fetch the value for a specific key
1756
	 *
1757
	 * @since 1.16.4
1758
	 *
1759
	 * @param string $name Name of the request key to fetch the value for
1760
	 *
1761
	 * @return mixed|string Value of request at $name key. Empty string if empty.
1762
	 */
1763
	private function rgget_or_rgpost( $name ) {
1764
		$value = \GV\Utils::_REQUEST( $name );
1765
1766
		$value = stripslashes_deep( $value );
1767
1768
		$value = gv_map_deep( $value, 'rawurldecode' );
1769
1770
		$value = gv_map_deep( $value, '_wp_specialchars' );
1771
1772
		return $value;
1773
	}
1774
1775
1776
	/**
1777
	 * Require the datepicker script for the frontend GV script
1778
	 * @param array $js_dependencies Array of existing required scripts for the fe-views.js script
1779
	 * @return array Array required scripts, with `jquery-ui-datepicker` added
1780
	 */
1781
	public function add_datepicker_js_dependency( $js_dependencies ) {
1782
1783
		$js_dependencies[] = 'jquery-ui-datepicker';
1784
1785
		return $js_dependencies;
1786
	}
1787
1788
	/**
1789
	 * Modify the array passed to wp_localize_script()
1790
	 *
1791
	 * @param array $js_localization The data padded to the Javascript file
0 ignored issues
show
Bug introduced by
There is no parameter named $js_localization. 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...
1792
	 * @param array $view_data View data array with View settings
1793
	 *
1794
	 * @return array
1795
	 */
1796
	public function add_datepicker_localization( $localizations = array(), $view_data = array() ) {
1797
		global $wp_locale;
1798
1799
		/**
1800
		 * @filter `gravityview_datepicker_settings` Modify the datepicker settings
1801
		 * @see http://api.jqueryui.com/datepicker/ Learn what settings are available
1802
		 * @see http://www.renegadetechconsulting.com/tutorials/jquery-datepicker-and-wordpress-i18n Thanks for the helpful information on $wp_locale
1803
		 * @param array $js_localization The data padded to the Javascript file
1804
		 * @param array $view_data View data array with View settings
1805
		 */
1806
		$datepicker_settings = apply_filters( 'gravityview_datepicker_settings', array(
1807
			'yearRange' => '-5:+5',
1808 4
			'changeMonth' => true,
1809 4
			'changeYear' => true,
1810
			'closeText' => esc_attr_x( 'Close', 'Close calendar', 'gravityview' ),
1811
			'prevText' => esc_attr_x( 'Prev', 'Previous month in calendar', 'gravityview' ),
1812 4
			'nextText' => esc_attr_x( 'Next', 'Next month in calendar', 'gravityview' ),
1813
			'currentText' => esc_attr_x( 'Today', 'Today in calendar', 'gravityview' ),
1814
			'weekHeader' => esc_attr_x( 'Week', 'Week in calendar', 'gravityview' ),
1815
			'monthStatus'       => __( 'Show a different month', 'gravityview' ),
1816
			'monthNames'        => array_values( $wp_locale->month ),
1817
			'monthNamesShort'   => array_values( $wp_locale->month_abbrev ),
1818
			'dayNames'          => array_values( $wp_locale->weekday ),
1819
			'dayNamesShort'     => array_values( $wp_locale->weekday_abbrev ),
1820
			'dayNamesMin'       => array_values( $wp_locale->weekday_initial ),
1821
			// get the start of week from WP general setting
1822
			'firstDay'          => get_option( 'start_of_week' ),
1823
			// is Right to left language? default is false
1824
			'isRTL'             => is_rtl(),
1825
		), $view_data );
1826
1827
		$localizations['datepicker'] = $datepicker_settings;
1828
1829
		return $localizations;
1830
1831
	}
1832
1833
	/**
1834
	 * Register search widget scripts, including Flexibility
1835
	 *
1836
	 * @see https://github.com/10up/flexibility
1837
	 *
1838
	 * @since 1.17
1839
	 *
1840
	 * @return void
1841
	 */
1842
	public function register_scripts() {
1843
		wp_register_script( 'gv-flexibility', plugins_url( 'assets/lib/flexibility/flexibility.js', GRAVITYVIEW_FILE ), array(), \GV\Plugin::$version, true );
1844
	}
1845
1846
	/**
1847
	 * If the current visitor is running IE 8 or 9, enqueue Flexibility
1848
	 *
1849
	 * @since 1.17
1850
	 *
1851
	 * @return void
1852
	 */
1853
	private function maybe_enqueue_flexibility() {
1854
		if ( isset( $_SERVER['HTTP_USER_AGENT'] ) && preg_match( '/MSIE [8-9]/', $_SERVER['HTTP_USER_AGENT'] ) ) {
1855
			wp_enqueue_script( 'gv-flexibility' );
1856
		}
1857
	}
1858
1859
	/**
1860 19
	 * Enqueue the datepicker script
1861
	 *
1862 19
	 * It sets the $gravityview->datepicker_class parameter
1863
	 *
1864
	 * @todo Use own datepicker javascript instead of GF datepicker.js - that way, we can localize the settings and not require the changeMonth and changeYear pickers.
1865
	 * @return void
1866
	 */
1867
	public function enqueue_datepicker() {
1868
		$gravityview_view = GravityView_View::getInstance();
1869
1870
		wp_enqueue_script( 'jquery-ui-datepicker' );
1871
1872
		add_filter( 'gravityview_js_dependencies', array( $this, 'add_datepicker_js_dependency' ) );
1873
		add_filter( 'gravityview_js_localization', array( $this, 'add_datepicker_localization' ), 10, 2 );
1874
1875
		$scheme = is_ssl() ? 'https://' : 'http://';
1876
		wp_enqueue_style( 'jquery-ui-datepicker', $scheme.'ajax.googleapis.com/ajax/libs/jqueryui/1.8.18/themes/smoothness/jquery-ui.css' );
1877 19
1878
		/**
1879
		 * @filter `gravityview_search_datepicker_class`
1880 19
		 * Modify the CSS class for the datepicker, used by the CSS class is used by Gravity Forms' javascript to determine the format for the date picker. The `gv-datepicker` class is required by the GravityView datepicker javascript.
1881
		 * @param string $css_class CSS class to use. Default: `gv-datepicker datepicker mdy` \n
1882
		 * Options are:
1883
		 * - `mdy` (mm/dd/yyyy)
1884
		 * - `dmy` (dd/mm/yyyy)
1885
		 * - `dmy_dash` (dd-mm-yyyy)
1886
		 * - `dmy_dot` (dd.mm.yyyy)
1887
		 * - `ymd_slash` (yyyy/mm/dd)
1888
		 * - `ymd_dash` (yyyy-mm-dd)
1889
		 * - `ymd_dot` (yyyy.mm.dd)
1890
		 */
1891 19
		$datepicker_class = apply_filters( 'gravityview_search_datepicker_class', "gv-datepicker datepicker " . $this->get_datepicker_format() );
1892
1893
		$gravityview_view->datepicker_class = $datepicker_class;
0 ignored issues
show
Bug introduced by
The property datepicker_class does not seem to exist in GravityView_View.

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
1894
	}
1895
1896
	/**
1897 19
	 * Retrieve the datepicker format.
1898
	 *
1899
	 * @param bool $date_format Whether to return the PHP date format or the datpicker class name. Default: false.
1900
	 *
1901
	 * @see https://docs.gravityview.co/article/115-changing-the-format-of-the-search-widgets-date-picker
1902
	 *
1903
	 * @return string The datepicker format placeholder, or the PHP date format.
1904
	 */
1905
	private function get_datepicker_format( $date_format = false ) {
1906
1907 4
		$default_format = 'mdy';
1908 4
1909
		/**
1910 4
		 * @filter `gravityview/widgets/search/datepicker/format`
1911 4
		 * @since 2.1.1
1912
		 * @param string           $format Default: mdy
1913
		 * Options are:
1914
		 * - `mdy` (mm/dd/yyyy)
1915
		 * - `dmy` (dd/mm/yyyy)
1916
		 * - `dmy_dash` (dd-mm-yyyy)
1917
		 * - `dmy_dot` (dd.mm.yyyy)
1918
		 * - `ymd_slash` (yyyy/mm/dd)
1919
		 * - `ymd_dash` (yyyy-mm-dd)
1920
		 * - `ymd_dot` (yyyy.mm.dd)
1921
		 */
1922
		$format = apply_filters( 'gravityview/widgets/search/datepicker/format', $default_format );
1923
1924
		$gf_date_formats = array(
1925
			'mdy' => 'm/d/Y',
1926
1927
			'dmy_dash' => 'd-m-Y',
1928
			'dmy_dot' => 'd.m.Y',
1929
			'dmy' => 'd/m/Y',
1930
1931 19
			'ymd_slash' => 'Y/m/d',
1932 19
			'ymd_dash' => 'Y-m-d',
1933
			'ymd_dot' => 'Y.m.d',
1934
		);
1935
1936
		if ( ! $date_format ) {
1937
			// If the format key isn't valid, return default format key
1938
			return isset( $gf_date_formats[ $format ] ) ? $format : $default_format;
1939 19
		}
1940
1941 19
		// If the format key isn't valid, return default format value
1942 1
		return \GV\Utils::get( $gf_date_formats, $format, $gf_date_formats[ $default_format ] );
1943
	}
1944
1945 19
	/**
1946
	 * If previewing a View or page with embedded Views, make the search work properly by adding hidden fields with query vars
1947
	 *
1948
	 * @since 2.2.1
1949
	 *
1950
	 * @return void
1951
	 */
1952
	public function add_preview_inputs() {
1953
		global $wp;
1954
1955
		if ( ! is_preview() || ! current_user_can( 'publish_gravityviews') ) {
1956
			return;
1957
		}
1958
1959
		// Outputs `preview` and `post_id` variables
1960
		foreach ( $wp->query_vars as $key => $value ) {
1961 1
			printf( '<input type="hidden" name="%s" value="%s" />', esc_attr( $key ), esc_attr( $value ) );
1962 1
		}
1963 1
1964 1
	}
1965
1966 1
	/**
1967 1
	 * Get an operator URL override.
1968
	 *
1969
	 * @param array  $get     Where to look for the operator.
1970 1
	 * @param string $key     The filter key to look for.
1971
	 * @param array  $allowed The allowed operators (whitelist).
1972
	 * @param string $default The default operator.
1973
	 *
1974
	 * @return string The operator.
1975
	 */
1976
	private function get_operator( $get, $key, $allowed, $default ) {
1977
		$operator = \GV\Utils::get( $get, "$key|op", $default );
1978 1
1979
		/**
1980
		 * @filter `gravityview/search/operator_whitelist` An array of allowed operators for a field.
1981 1
		 * @param[in,out] string[] A whitelist of allowed operators.
1982
		 * @param string The filter name.
1983
		 */
1984
		$allowed = apply_filters( 'gravityview/search/operator_whitelist', $allowed, $key );
1985
1986
		if ( ! in_array( $operator, $allowed, true ) ) {
1987
			$operator = $default;
1988
		}
1989 1
1990
		return $operator;
1991 1
	}
1992
1993 1
1994 1
} // end class
1995
1996
new GravityView_Widget_Search;
1997 1
1998 1
if ( ! gravityview()->plugin->supports( \GV\Plugin::FEATURE_GFQUERY ) ) {
1999
	return;
2000
}
2001 1
2002
/**
2003 1
 * A GF_Query condition that allows user data searches.
2004
 */
2005 1
class GravityView_Widget_Search_Author_GF_Query_Condition extends \GF_Query_Condition {
2006
	public function __construct( $filter, $view ) {
2007
		$this->value = $filter['value'];
2008
		$this->view = $view;
2009
	}
2010
2011
	public function sql( $query ) {
2012
		global $wpdb;
2013
2014
		$user_meta_fields = array(
2015
			'nickname', 'first_name', 'last_name',
2016
		);
2017
2018
		/**
2019
		 * @filter `gravityview/widgets/search/created_by/user_meta_fields` Filter the user meta fields to search.
2020
		 * @param[in,out] array The user meta fields.
2021
		 * @param \GV\View $view The view.
2022
		 */
2023
		$user_meta_fields = apply_filters( 'gravityview/widgets/search/created_by/user_meta_fields', $user_meta_fields, $this->view );
2024
2025
		$user_fields = array(
2026
			'user_nicename', 'user_login', 'display_name', 'user_email',
2027
		);
2028
2029
		/**
2030
		 * @filter `gravityview/widgets/search/created_by/user_fields` Filter the user fields to search.
2031
		 * @param[in,out] array The user fields.
2032
		 * @param \GV\View $view The view.
2033
		 */
2034
		$user_fields = apply_filters( 'gravityview/widgets/search/created_by/user_fields', $user_fields, $this->view );
2035
2036
		$conditions = array();
2037
2038
		foreach ( $user_fields as $user_field ) {
2039
			$conditions[] = $wpdb->prepare( "`u`.`$user_field` LIKE %s", '%' . $wpdb->esc_like( $this->value ) .  '%' );
2040
		}
2041
2042
		foreach ( $user_meta_fields as $meta_field ) {
2043
			$conditions[] = $wpdb->prepare( "(`um`.`meta_key` = %s AND `um`.`meta_value` LIKE %s)", $meta_field, '%' . $wpdb->esc_like( $this->value ) .  '%' );
2044
		}
2045
2046
		$conditions = '(' . implode( ' OR ', $conditions ) . ')';
2047
2048
		$alias = $query->_alias( null );
2049
2050
		return "(EXISTS (SELECT 1 FROM $wpdb->users u LEFT JOIN $wpdb->usermeta um ON u.ID = um.user_id WHERE (u.ID = `$alias`.`created_by` AND $conditions)))";
2051
	}
2052
}
2053