Completed
Push — master ( 4ba683...3a59f6 )
by Zack
31:21 queued 27:45
created

GravityView_Widget_Search::get_operator()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 16

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 2

Importance

Changes 0
Metric Value
cc 2
nc 2
nop 4
dl 0
loc 16
ccs 6
cts 6
cp 1
crap 2
rs 9.7333
c 0
b 0
f 0
1
<?php
0 ignored issues
show
Coding Style Compatibility introduced by
For compatibility and reusability of your code, PSR1 recommends that a file should introduce either new symbols (like classes, functions, etc.) or have side-effects (like outputting something, or including other files), but not both at the same time. The first symbol is defined on line 16 and the first side effect is on line 13.

The PSR-1: Basic Coding Standard recommends that a file should either introduce new symbols, that is classes, functions, constants or similar, or have side effects. Side effects are anything that executes logic, like for example printing output, changing ini settings or writing to a file.

The idea behind this recommendation is that merely auto-loading a class should not change the state of an application. It also promotes a cleaner style of programming and makes your code less prone to errors, because the logic is not spread out all over the place.

To learn more about the PSR-1, please see the PHP-FIG site on the PSR-1.

Loading history...
2
/**
3
 * The GravityView New Search widget
4
 *
5
 * @package   GravityView-DataTables-Ext
6
 * @license   GPL2+
7
 * @author    Katz Web Services, Inc.
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();
0 ignored issues
show
Unused Code introduced by
The property $search_filters is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
22
23
	/**
24
	 * whether search method is GET or POST ( default: GET )
25
	 * @since 1.16.4
26
	 * @var string
27
	 */
28
	private $search_method = 'get';
29
30 34
	public function __construct() {
31
32 34
		$this->widget_id = 'search_bar';
33 34
		$this->widget_description = esc_html__( 'Search form for searching entries.', 'gravityview' );
34
35 34
		self::$instance = &$this;
36
37 34
		self::$file = plugin_dir_path( __FILE__ );
38
39 34
		$default_values = array( 'header' => 0, 'footer' => 0 );
40
41
		$settings = array(
42 34
			'search_layout' => array(
43 34
				'type' => 'radio',
44
				'full_width' => true,
45 34
				'label' => esc_html__( 'Search Layout', 'gravityview' ),
46 34
				'value' => 'horizontal',
47
				'options' => array(
48 34
					'horizontal' => esc_html__( 'Horizontal', 'gravityview' ),
49 34
					'vertical' => esc_html__( 'Vertical', 'gravityview' ),
50
				),
51
			),
52
			'search_clear' => array(
53 34
				'type' => 'checkbox',
54 34
				'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 34
				'type' => 'radio',
65
				'full_width' => true,
66 34
				'label' => esc_html__( 'Search Mode', 'gravityview' ),
67 34
				'desc' => __('Should search results match all search fields, or any?', 'gravityview'),
0 ignored issues
show
Coding Style introduced by
Expected 1 spaces after opening bracket; 0 found
Loading history...
Coding Style introduced by
Expected 1 spaces before closing bracket; 0 found
Loading history...
68 34
				'value' => 'any',
69 34
				'class' => 'hide-if-js',
70
				'options' => array(
71 34
					'any' => esc_html__( 'Match Any Fields', 'gravityview' ),
72 34
					'all' => esc_html__( 'Match All Fields', 'gravityview' ),
73
				),
74
			),
75
		);
76
77 34
		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
			// Add hidden fields for "Default" permalink structure
85
			add_filter( 'gravityview_widget_search_filters', array( $this, 'add_no_permalink_fields' ), 10, 3 );
86
87
			// admin - add scripts - run at 1100 to make sure GravityView_Admin_Views::add_scripts_and_styles() runs first at 999
88
			add_action( 'admin_enqueue_scripts', array( $this, 'add_scripts_and_styles' ), 1100 );
89
			add_action( 'wp_enqueue_scripts', array( $this, 'register_scripts') );
0 ignored issues
show
introduced by
No space before closing parenthesis of array is bad style
Loading history...
90
			add_filter( 'gravityview_noconflict_scripts', array( $this, 'register_no_conflict' ) );
91
92
			// ajax - get the searchable fields
93
			add_action( 'wp_ajax_gv_searchable_fields', array( 'GravityView_Widget_Search', 'get_searchable_fields' ) );
94
95
			add_action( 'gravityview_search_widget_fields_after', array( $this, 'add_preview_inputs' ) );
96
		}
97
98 34
		parent::__construct( esc_html__( 'Search Bar', 'gravityview' ), null, $default_values, $settings );
99
100
		// calculate the search method (POST / GET)
101 34
		$this->set_search_method();
102 34
	}
103
104
	/**
105
	 * @return GravityView_Widget_Search
106
	 */
107 5
	public static function getInstance() {
0 ignored issues
show
Coding Style introduced by
The function name getInstance is in camel caps, but expected get_instance instead as per the coding standard.
Loading history...
108 5
		if ( empty( self::$instance ) ) {
109
			self::$instance = new GravityView_Widget_Search;
110
		}
111 5
		return self::$instance;
112
	}
113
114
	/**
115
	 * Sets the search method to GET (default) or POST
116
	 * @since 1.16.4
117
	 */
118 34
	private function set_search_method() {
119
		/**
120
		 * @filter `gravityview/search/method` Modify the search form method (GET / POST)
121
		 * @since 1.16.4
122
		 * @param string $search_method Assign an input type according to the form field type. Defaults: `boolean`, `multi`, `select`, `date`, `text`
123
		 * @param string $field_type Gravity Forms field type (also the `name` parameter of GravityView_Field classes)
124
		 */
125 34
		$method = apply_filters( 'gravityview/search/method', $this->search_method );
126
127 34
		$method = strtolower( $method );
128
129 34
		$this->search_method = in_array( $method, array( 'get', 'post' ) ) ? $method : 'get';
130 34
	}
131
132
	/**
133
	 * Returns the search method
134
	 * @since 1.16.4
135
	 * @return string
136
	 */
137 5
	public function get_search_method() {
138 5
		return $this->search_method;
139
	}
140
141
	/**
142
	 * Get the input types available for different field types
143
	 *
144
	 * @since 1.17.5
145
	 *
146
	 * @return array [field type name] => (array|string) search bar input types
147
	 */
148
	public static function get_input_types_by_field_type() {
149
		/**
150
		 * Input Type groups
151
		 * @see admin-search-widget.js (getSelectInput)
152
		 * @var array
153
		 */
154
		$input_types = array(
155
			'text' => array( 'input_text' ),
156
			'address' => array( 'input_text' ),
157
			'number' => array( 'input_text' ),
158
			'date' => array( 'date', 'date_range' ),
159
			'boolean' => array( 'single_checkbox' ),
160
			'select' => array( 'select', 'radio', 'link' ),
161
			'multi' => array( 'select', 'multiselect', 'radio', 'checkbox', 'link' ),
162
163
			// hybrids
164
			'created_by' => array( 'select', 'radio', 'checkbox', 'multiselect', 'link', 'input_text' ),
165
			'product'   => array( 'select', 'radio', 'link', 'input_text' ),
166
		);
167
168
		/**
169
		 * @filter `gravityview/search/input_types` Change the types of search fields available to a field type
170
		 * @see GravityView_Widget_Search::get_search_input_labels() for the available input types
171
		 * @param array $input_types Associative array: key is field `name`, value is array of GravityView input types (note: use `input_text` for `text`)
172
		 */
173
		$input_types = apply_filters( 'gravityview/search/input_types', $input_types );
174
175
		return $input_types;
176
	}
177
178
	/**
179
	 * Get labels for different types of search bar inputs
180
	 *
181
	 * @since 1.17.5
182
	 *
183
	 * @return array [input type] => input type label
184
	 */
185
	public static function get_search_input_labels() {
186
		/**
187
		 * Input Type labels l10n
188
		 * @see admin-search-widget.js (getSelectInput)
189
		 * @var array
190
		 */
191
		$input_labels = array(
192
			'input_text' => esc_html__( 'Text', 'gravityview' ),
193
			'date' => esc_html__( 'Date', 'gravityview' ),
194
			'select' => esc_html__( 'Select', 'gravityview' ),
195
			'multiselect' => esc_html__( 'Select (multiple values)', 'gravityview' ),
196
			'radio' => esc_html__( 'Radio', 'gravityview' ),
197
			'checkbox' => esc_html__( 'Checkbox', 'gravityview' ),
198
			'single_checkbox' => esc_html__( 'Checkbox', 'gravityview' ),
199
			'link' => esc_html__( 'Links', 'gravityview' ),
200
			'date_range' => esc_html__( 'Date range', 'gravityview' ),
201
		);
202
203
		/**
204
		 * @filter `gravityview/search/input_types` Change the label of search field input types
205
		 * @param array $input_types Associative array: key is input type name, value is label
206
		 */
207
		$input_labels = apply_filters( 'gravityview/search/input_labels', $input_labels );
208
209
		return $input_labels;
210
	}
211
212
	public static function get_search_input_label( $input_type ) {
213
		$labels = self::get_search_input_labels();
214
215
		return \GV\Utils::get( $labels, $input_type, false );
216
	}
217
218
	/**
219
	 * Add script to Views edit screen (admin)
220
	 * @param  mixed $hook
221
	 */
222
	public function add_scripts_and_styles( $hook ) {
223
		global $pagenow;
0 ignored issues
show
Compatibility Best Practice introduced by
Use of global functionality is not recommended; it makes your code harder to test, and less reusable.

Instead of relying on global state, we recommend one of these alternatives:

1. Pass all data via parameters

function myFunction($a, $b) {
    // Do something
}

2. Create a class that maintains your state

class MyClass {
    private $a;
    private $b;

    public function __construct($a, $b) {
        $this->a = $a;
        $this->b = $b;
    }

    public function myFunction() {
        // Do something
    }
}
Loading history...
224
225
		// Don't process any scripts below here if it's not a GravityView page or the widgets screen
226
		if ( ! gravityview()->request->is_admin( $hook, 'single' ) && ( 'widgets.php' !== $pagenow ) ) {
0 ignored issues
show
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...
227
			return;
228
		}
229
230
		$script_min = ( defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ) ? '' : '.min';
231
		$script_source = empty( $script_min ) ? '/source' : '';
232
233
		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 );
234
235
		wp_localize_script( 'gravityview_searchwidget_admin', 'gvSearchVar', array(
236
			'nonce' => wp_create_nonce( 'gravityview_ajaxsearchwidget' ),
237
			'label_nofields' => esc_html__( 'No search fields configured yet.', 'gravityview' ),
238
			'label_addfield' => esc_html__( 'Add Search Field', 'gravityview' ),
239
			'label_label' => esc_html__( 'Label', 'gravityview' ),
240
			'label_searchfield' => esc_html__( 'Search Field', 'gravityview' ),
241
			'label_inputtype' => esc_html__( 'Input Type', 'gravityview' ),
242
			'label_ajaxerror' => esc_html__( 'There was an error loading searchable fields. Save the View or refresh the page to fix this issue.', 'gravityview' ),
243
			'input_labels' => json_encode( self::get_search_input_labels() ),
244
			'input_types' => json_encode( self::get_input_types_by_field_type() ),
245
		) );
246
247
	}
248
249
	/**
250
	 * Add admin script to the no-conflict scripts whitelist
251
	 * @param array $allowed Scripts allowed in no-conflict mode
252
	 * @return array Scripts allowed in no-conflict mode, plus the search widget script
253
	 */
254
	public function register_no_conflict( $allowed ) {
255
		$allowed[] = 'gravityview_searchwidget_admin';
256
		return $allowed;
257
	}
258
259
	/**
260
	 * Ajax
261
	 * Returns the form fields ( only the searchable ones )
262
	 *
263
	 * @access public
264
	 * @return void
265
	 */
266
	public static function get_searchable_fields() {
267
268
		if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( $_POST['nonce'], 'gravityview_ajaxsearchwidget' ) ) {
0 ignored issues
show
introduced by
Detected access of super global var $_POST, probably need manual inspection.
Loading history...
introduced by
Detected usage of a non-sanitized input variable: $_POST
Loading history...
269
			exit( '0' );
0 ignored issues
show
Coding Style Compatibility introduced by
The method get_searchable_fields() contains an exit expression.

An exit expression should only be used in rare cases. For example, if you write a short command line script.

In most cases however, using an exit expression makes the code untestable and often causes incompatibilities with other libraries. Thus, unless you are absolutely sure it is required here, we recommend to refactor your code to avoid its usage.

Loading history...
270
		}
271
272
		$form = '';
273
274
		// Fetch the form for the current View
275
		if ( ! empty( $_POST['view_id'] ) ) {
276
277
			$form = gravityview_get_form_id( $_POST['view_id'] );
0 ignored issues
show
introduced by
Detected access of super global var $_POST, probably need manual inspection.
Loading history...
introduced by
Detected usage of a non-sanitized input variable: $_POST
Loading history...
278
279
		} elseif ( ! empty( $_POST['formid'] ) ) {
280
281
			$form = (int) $_POST['formid'];
0 ignored issues
show
introduced by
Detected access of super global var $_POST, probably need manual inspection.
Loading history...
282
283
		} elseif ( ! empty( $_POST['template_id'] ) && class_exists( 'GravityView_Ajax' ) ) {
284
285
			$form = GravityView_Ajax::pre_get_form_fields( $_POST['template_id'] );
0 ignored issues
show
introduced by
Detected access of super global var $_POST, probably need manual inspection.
Loading history...
introduced by
Detected usage of a non-sanitized input variable: $_POST
Loading history...
286
287
		}
288
289
		// fetch form id assigned to the view
290
		$response = self::render_searchable_fields( $form );
291
292
		exit( $response );
0 ignored issues
show
Coding Style Compatibility introduced by
The method get_searchable_fields() contains an exit expression.

An exit expression should only be used in rare cases. For example, if you write a short command line script.

In most cases however, using an exit expression makes the code untestable and often causes incompatibilities with other libraries. Thus, unless you are absolutely sure it is required here, we recommend to refactor your code to avoid its usage.

Loading history...
293
	}
294
295
	/**
296
	 * Generates html for the available Search Fields dropdown
297
	 * @param  int $form_id
298
	 * @param  string $current (for future use)
299
	 * @return string
300
	 */
301
	public static function render_searchable_fields( $form_id = null, $current = '' ) {
302
303
		if ( is_null( $form_id ) ) {
304
			return '';
305
		}
306
307
		// start building output
308
309
		$output = '<select class="gv-search-fields">';
310
311
		$custom_fields = array(
312
			'search_all' => array(
313
				'text' => esc_html__( 'Search Everything', 'gravityview' ),
314
				'type' => 'text',
315
			),
316
			'entry_date' => array(
317
				'text' => esc_html__( 'Entry Date', 'gravityview' ),
318
				'type' => 'date',
319
			),
320
			'entry_id' => array(
321
				'text' => esc_html__( 'Entry ID', 'gravityview' ),
322
				'type' => 'text',
323
			),
324
			'created_by' => array(
325
				'text' => esc_html__( 'Entry Creator', 'gravityview' ),
326
				'type' => 'created_by',
327
			),
328
			'is_starred' => array(
329
				'text' => esc_html__( 'Is Starred', 'gravityview' ),
330
				'type' => 'boolean',
331
			),
332
		);
333
334
		if ( gravityview()->plugin->supports( \GV\Plugin::FEATURE_GFQUERY ) ) {
335
			$custom_fields['is_approved'] = array(
336
				'text' => esc_html__( 'Approval Status', 'gravityview' ),
337
				'type' => 'multi',
338
			);
339
		}
340
341
		foreach( $custom_fields as $custom_field_key => $custom_field ) {
342
			$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'] );
0 ignored issues
show
introduced by
No space after opening parenthesis of array is bad style
Loading history...
343
		}
344
345
		// Get fields with sub-inputs and no parent
346
		$fields = gravityview_get_form_fields( $form_id, true, true );
347
348
		/**
349
		 * @filter `gravityview/search/searchable_fields` Modify the fields that are displayed as searchable in the Search Bar dropdown\n
350
		 * @since 1.17
351
		 * @see gravityview_get_form_fields() Used to fetch the fields
352
		 * @see GravityView_Widget_Search::get_search_input_types See this method to modify the type of input types allowed for a field
353
		 * @param array $fields Array of searchable fields, as fetched by gravityview_get_form_fields()
354
		 * @param  int $form_id
355
		 */
356
		$fields = apply_filters( 'gravityview/search/searchable_fields', $fields, $form_id );
357
358
		if ( ! empty( $fields ) ) {
359
360
			$blacklist_field_types = apply_filters( 'gravityview_blacklist_field_types', array( 'fileupload', 'post_image', 'post_id', 'section' ), null );
361
362
			foreach ( $fields as $id => $field ) {
363
364
				if ( in_array( $field['type'], $blacklist_field_types ) ) {
365
					continue;
366
				}
367
368
				$types = self::get_search_input_types( $id, $field['type'] );
369
370
				$output .= '<option value="'. $id .'" '. selected( $id, $current, false ).'data-inputtypes="'. esc_attr( $types ) .'">'. esc_html( $field['label'] ) .'</option>';
371
			}
372
		}
373
374
		$output .= '</select>';
375
376
		return $output;
377
378
	}
379
380
	/**
381
	 * Assign an input type according to the form field type
382
	 *
383
	 * @see admin-search-widget.js
384
	 *
385
	 * @param string|int|float $field_id Gravity Forms field ID
386
	 * @param string $field_type Gravity Forms field type (also the `name` parameter of GravityView_Field classes)
387
	 *
388
	 * @return string GV field search input type ('multi', 'boolean', 'select', 'date', 'text')
389
	 */
390
	public static function get_search_input_types( $field_id = '', $field_type = null ) {
391
392
		// @todo - This needs to be improved - many fields have . including products and addresses
393
		if ( false !== strpos( (string) $field_id, '.' ) && in_array( $field_type, array( 'checkbox' ) ) || in_array( $field_id, array( 'is_fulfilled' ) ) ) {
394
			$input_type = 'boolean'; // on/off checkbox
395
		} elseif ( in_array( $field_type, array( 'checkbox', 'post_category', 'multiselect' ) ) ) {
396
			$input_type = 'multi'; //multiselect
397
		} elseif ( in_array( $field_type, array( 'select', 'radio' ) ) ) {
398
			$input_type = 'select';
399
		} elseif ( in_array( $field_type, array( 'date' ) ) || in_array( $field_id, array( 'payment_date' ) ) ) {
400
			$input_type = 'date';
401
		} elseif ( in_array( $field_type, array( 'number' ) ) || in_array( $field_id, array( 'payment_amount' ) ) ) {
402
			$input_type = 'number';
403
		} elseif ( in_array( $field_type, array( 'product' ) ) ) {
404
			$input_type = 'product';
405
		} else {
406
			$input_type = 'text';
407
		}
408
409
		/**
410
		 * @filter `gravityview/extension/search/input_type` Modify the search form input type based on field type
411
		 * @since 1.2
412
		 * @since 1.19.2 Added $field_id parameter
413
		 * @param string $input_type Assign an input type according to the form field type. Defaults: `boolean`, `multi`, `select`, `date`, `text`
414
		 * @param string $field_type Gravity Forms field type (also the `name` parameter of GravityView_Field classes)
415
		 * @param string|int|float $field_id ID of the field being processed
416
		 */
417
		$input_type = apply_filters( 'gravityview/extension/search/input_type', $input_type, $field_type, $field_id );
418
419
		return $input_type;
420
	}
421
422
	/**
423
	 * Display hidden fields to add support for sites using Default permalink structure
424
	 *
425
	 * @since 1.8
426
	 * @return array Search fields, modified if not using permalinks
427
	 */
428 4
	public function add_no_permalink_fields( $search_fields, $object, $widget_args = array() ) {
429
		/** @global WP_Rewrite $wp_rewrite */
430 4
		global $wp_rewrite;
0 ignored issues
show
Compatibility Best Practice introduced by
Use of global functionality is not recommended; it makes your code harder to test, and less reusable.

Instead of relying on global state, we recommend one of these alternatives:

1. Pass all data via parameters

function myFunction($a, $b) {
    // Do something
}

2. Create a class that maintains your state

class MyClass {
    private $a;
    private $b;

    public function __construct($a, $b) {
        $this->a = $a;
        $this->b = $b;
    }

    public function myFunction() {
        // Do something
    }
}
Loading history...
431
432
		// Support default permalink structure
433 4
		if ( false === $wp_rewrite->using_permalinks() ) {
434
435
			// By default, use current post.
436 4
			$post_id = 0;
437
438
			// We're in the WordPress Widget context, and an overriding post ID has been set.
439 4
			if ( ! empty( $widget_args['post_id'] ) ) {
440
				$post_id = absint( $widget_args['post_id'] );
441
			}
442
			// We're in the WordPress Widget context, and the base View ID should be used
443 4
			else if ( ! empty( $widget_args['view_id'] ) ) {
444
				$post_id = absint( $widget_args['view_id'] );
445
			}
446
447 4
			$args = gravityview_get_permalink_query_args( $post_id );
448
449
			// Add hidden fields to the search form
450 4
			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...
451 4
				$search_fields[] = array(
452 4
					'name'  => $key,
453 4
					'input' => 'hidden',
454 4
					'value' => $value,
455
				);
456
			}
457
		}
458
459 4
		return $search_fields;
460
	}
461
462
	/**
463
	 * Get the fields that are searchable for a View
464
	 *
465
	 * @since 2.0
466
	 * @since 2.0.9 Added $with_full_field parameter
467
	 *
468
	 * @param \GV\View|null $view
469
	 * @param bool $with_full_field Return full field array, or just field ID? Default: false (just field ID)
470
	 *
471
	 * TODO: Move to \GV\View, perhaps? And return a Field_Collection
472
	 * TODO: Use in gravityview()->request->is_search() to calculate whether a valid search
473
	 *
474
	 * @return array If no View, returns empty array. Otherwise, returns array of fields configured in widgets and Search Bar for a View
475
	 */
476 30
	private function get_view_searchable_fields( $view, $with_full_field = false ) {
477
478
		/**
479
		 * Find all search widgets on the view and get the searchable fields settings.
480
		 */
481 30
		$searchable_fields = array();
482
483 30
		if ( ! $view ) {
484
			return $searchable_fields;
485
		}
486
487
		/**
488
		 * Include the sidebar Widgets.
489
		 */
490 30
		$widgets = (array) get_option( 'widget_gravityview_search', array() );
491
492 30
		foreach ( $widgets as $widget ) {
493 30
			if ( ! empty( $widget['view_id'] ) && $widget['view_id'] == $view->ID ) {
494
				if( $_fields = json_decode( $widget['search_fields'], true ) ) {
495
					foreach ( $_fields as $field ) {
496
						if ( empty( $field['form_id'] ) ) {
497
							$field['form_id'] = $view->form ? $view->form->ID : 0;
498
						}
499
						$searchable_fields[] = $with_full_field ? $field : $field['field'];
500
					}
501
				}
502
			}
503
		}
504
505 30
		foreach ( $view->widgets->by_id( $this->get_widget_id() )->all() as $widget ) {
506 28
			if( $_fields = json_decode( $widget->configuration->get( 'search_fields' ), true ) ) {
507 28
				foreach ( $_fields as $field ) {
508 28
					if ( empty( $field['form_id'] ) ) {
509 28
						$field['form_id'] = $view->form ? $view->form->ID : 0;
510
					}
511 28
					$searchable_fields[] = $with_full_field ? $field : $field['field'];
512
				}
513
			}
514
		}
515
516 30
		return $searchable_fields;
517
	}
518
519
	/** --- Frontend --- */
520
521
	/**
522
	 * Calculate the search criteria to filter entries
523
	 * @param array $search_criteria The search criteria
524
	 * @param int $form_id The form ID
525
	 * @param array $args Some args
526
	 *
527
	 * @param bool $force_search_criteria Whether to suppress GF_Query filter, internally used in self::gf_query_filter
528
	 *
529
	 * @return array
530
	 */
531 72
	public function filter_entries( $search_criteria, $form_id = null, $args = array(), $force_search_criteria = false ) {
532 72
		if ( ! $force_search_criteria && gravityview()->plugin->supports( \GV\Plugin::FEATURE_GFQUERY ) ) {
533
			/**
534
			 * If GF_Query is available, we can construct custom conditions with nested
535
			 * booleans on the query, giving up the old ways of flat search_criteria field_filters.
536
			 */
537 53
			add_action( 'gravityview/view/query', array( $this, 'gf_query_filter' ), 10, 3 );
538 53
			return $search_criteria; // Return the original criteria, GF_Query modification kicks in later
539
		}
540
541 71
		if( 'post' === $this->search_method ) {
542
			$get = $_POST;
0 ignored issues
show
introduced by
Detected access of super global var $_POST, probably need manual inspection.
Loading history...
introduced by
Detected usage of a non-sanitized input variable: $_POST
Loading history...
543
		} else {
544 71
			$get = $_GET;
0 ignored issues
show
introduced by
Detected access of super global var $_GET, probably need manual inspection.
Loading history...
introduced by
Detected usage of a non-sanitized input variable: $_GET
Loading history...
545
		}
546
547 71
		$view = \GV\View::by_id( \GV\Utils::get( $args, 'id' ) );
548
549 71
		gravityview()->log->debug( 'Requested $_{method}: ', array( 'method' => $this->search_method, 'data' => $get ) );
550
551 71
		if ( empty( $get ) || ! is_array( $get ) ) {
552 49
			return $search_criteria;
553
		}
554
555 31
		$get = stripslashes_deep( $get );
556
557 31
		$get = gv_map_deep( $get, 'rawurldecode' );
558
559
		// Make sure array key is set up
560 31
		$search_criteria['field_filters'] = \GV\Utils::get( $search_criteria, 'field_filters', array() );
561
562 31
		$searchable_fields = $this->get_view_searchable_fields( $view );
563 31
		$searchable_field_objects = $this->get_view_searchable_fields( $view, true );
564
565
		// add free search
566 31
		if ( isset( $get['gv_search'] ) && '' !== $get['gv_search'] && in_array( 'search_all', $searchable_fields ) ) {
567
568 1
			$search_all_value = trim( $get['gv_search'] );
569
570
			/**
571
			 * @filter `gravityview/search-all-split-words` Search for each word separately or the whole phrase?
572
			 * @since 1.20.2
573
			 * @param bool $split_words True: split a phrase into words; False: search whole word only [Default: true]
574
			 */
575 1
			$split_words = apply_filters( 'gravityview/search-all-split-words', true );
576
577 1
			if ( $split_words ) {
578
579
				// Search for a piece
580 1
				$words = explode( ' ', $search_all_value );
581
582 1
				$words = array_filter( $words );
583
584
			} else {
585
586
				// Replace multiple spaces with one space
587 1
				$search_all_value = preg_replace( '/\s+/ism', ' ', $search_all_value );
588
589 1
				$words = array( $search_all_value );
590
			}
591
592 1
			foreach ( $words as $word ) {
593 1
				$search_criteria['field_filters'][] = array(
594 1
					'key' => null, // The field ID to search
595 1
					'value' => $word, // The value to search
596 1
					'operator' => 'contains', // What to search in. Options: `is` or `contains`
597
				);
598
			}
599
		}
600
601
		// start date & end date
602 31
		if ( in_array( 'entry_date', $searchable_fields ) ) {
603
			/**
604
			 * Get and normalize the dates according to the input format.
605
			 */
606 12
			if ( $curr_start = ! empty( $get['gv_start'] ) ? $get['gv_start'] : '' ) {
607 12
				if( $curr_start_date = date_create_from_format( $this->get_datepicker_format( true ), $curr_start ) ) {
608 12
					$curr_start = $curr_start_date->format( 'Y-m-d' );
609
				}
610
			}
611
612 12
			if ( $curr_end = ! empty( $get['gv_start'] ) ? ( ! empty( $get['gv_end'] ) ? $get['gv_end'] : '' ) : '' ) {
613 12
				if( $curr_end_date = date_create_from_format( $this->get_datepicker_format( true ), $curr_end ) ) {
614 12
					$curr_end = $curr_end_date->format( 'Y-m-d' );
615
				}
616
			}
617
618 12
			if ( $view ) {
619
				/**
620
				 * Override start and end dates if View is limited to some already.
621
				 */
622 12
				if ( $start_date = $view->settings->get( 'start_date' ) ) {
623 1
					if ( $start_timestamp = strtotime( $curr_start ) ) {
624 1
						$curr_start = $start_timestamp < strtotime( $start_date ) ? $start_date : $curr_start;
625
					}
626
				}
627 12
				if ( $end_date = $view->settings->get( 'end_date' ) ) {
628
					if ( $end_timestamp = strtotime( $curr_end ) ) {
629
						$curr_end = $end_timestamp > strtotime( $end_date ) ? $end_date : $curr_end;
630
					}
631
				}
632
			}
633
634
			/**
635
			 * @filter `gravityview_date_created_adjust_timezone` Whether to adjust the timezone for entries. \n
636
			 * date_created is stored in UTC format. Convert search date into UTC (also used on templates/fields/date_created.php)
637
			 * @since 1.12
638
			 * @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: true
639
			 * @param[in] string $context Where the filter is being called from. `search` in this case.
640
			 */
641 12
			$adjust_tz = apply_filters( 'gravityview_date_created_adjust_timezone', true, 'search' );
642
643
			/**
644
			 * Don't set $search_criteria['start_date'] if start_date is empty as it may lead to bad query results (GFAPI::get_entries)
645
			 */
646 12
			if ( ! empty( $curr_start ) ) {
647 12
				$curr_start = date( 'Y-m-d H:i:s', strtotime( $curr_start ) );
648 12
				$search_criteria['start_date'] = $adjust_tz ? get_gmt_from_date( $curr_start ) : $curr_start;
649
			}
650
651 12
			if ( ! empty( $curr_end ) ) {
652
				// Fast-forward 24 hour on the end time
653 12
				$curr_end = date( 'Y-m-d H:i:s', strtotime( $curr_end ) + DAY_IN_SECONDS );
654 12
				$search_criteria['end_date'] = $adjust_tz ? get_gmt_from_date( $curr_end ) : $curr_end;
655 12
				if ( strpos( $search_criteria['end_date'], '00:00:00' ) ) { // See https://github.com/gravityview/GravityView/issues/1056
656 12
					$search_criteria['end_date'] = date( 'Y-m-d H:i:s', strtotime( $search_criteria['end_date'] ) - 1 );
657
				}
658
			}
659
		}
660
661
		// search for a specific entry ID
662 31
		if ( ! empty( $get[ 'gv_id' ] ) && in_array( 'entry_id', $searchable_fields ) ) {
0 ignored issues
show
introduced by
Array keys should NOT be surrounded by spaces if they only contain a string or an integer.
Loading history...
663 2
			$search_criteria['field_filters'][] = array(
664 2
				'key' => 'id',
665 2
				'value' => absint( $get[ 'gv_id' ] ),
0 ignored issues
show
introduced by
Array keys should NOT be surrounded by spaces if they only contain a string or an integer.
Loading history...
666 2
				'operator' => $this->get_operator( $get, 'gv_id', array( '=' ), '=' ),
667
			);
668
		}
669
670
		// search for a specific Created_by ID
671 31
		if ( ! empty( $get[ 'gv_by' ] ) && in_array( 'created_by', $searchable_fields ) ) {
0 ignored issues
show
introduced by
Array keys should NOT be surrounded by spaces if they only contain a string or an integer.
Loading history...
672 4
			$search_criteria['field_filters'][] = array(
673 4
				'key' => 'created_by',
674 4
				'value' => $get['gv_by'],
675 4
				'operator' => $this->get_operator( $get, 'gv_by', array( '=' ), '=' ),
676
			);
677
		}
678
679
		// Get search mode passed in URL
680 31
		$mode = isset( $get['mode'] ) && in_array( $get['mode'], array( 'any', 'all' ) ) ?  $get['mode'] : 'any';
681
682
		// get the other search filters
683 31
		foreach ( $get as $key => $value ) {
684
685 31
			if ( 0 !== strpos( $key, 'filter_' ) || gv_empty( $value, false, false ) || ( is_array( $value ) && count( $value ) === 1 && gv_empty( $value[0], false, false ) ) ) {
0 ignored issues
show
introduced by
Found "=== 1". Use Yoda Condition checks, you must
Loading history...
686 18
				continue; // Not a filter, or empty
687
			}
688
689 16
			if ( strpos( $key, '|op' ) !== false ) {
0 ignored issues
show
introduced by
Found "!== false". Use Yoda Condition checks, you must
Loading history...
690 1
				continue; // This is an operator
691
			}
692
693 16
			$filter_key = $this->convert_request_key_to_filter_key( $key );
694
695 16
			if ( ! $filter = $this->prepare_field_filter( $filter_key, $value, $view, $searchable_field_objects, $get ) ) {
696 2
				continue;
697
			}
698
699 15
			if ( ! isset( $filter['operator'] ) ) {
700 6
				$filter['operator'] = $this->get_operator( $get, $key, array( 'contains' ), 'contains' );
701
			}
702
703 15
			if ( isset( $filter[0]['value'] ) ) {
704
				$search_criteria['field_filters'] = array_merge( $search_criteria['field_filters'], $filter );
705
706
				// if date range type, set search mode to ALL
707
				if ( ! empty( $filter[0]['operator'] ) && in_array( $filter[0]['operator'], array( '>=', '<=', '>', '<' ) ) ) {
708
					$mode = 'all';
709
				}
710 15
			} elseif( !empty( $filter ) ) {
0 ignored issues
show
introduced by
Expected 1 space after "!"; 0 found
Loading history...
711 15
				$search_criteria['field_filters'][] = $filter;
712
			}
713
		}
714
715
		/**
716
		 * @filter `gravityview/search/mode` Set the Search Mode (`all` or `any`)
717
		 * @since 1.5.1
718
		 * @param[out,in] string $mode Search mode (`any` vs `all`)
719
		 */
720 31
		$search_criteria['field_filters']['mode'] = apply_filters( 'gravityview/search/mode', $mode );
721
722 31
		gravityview()->log->debug( 'Returned Search Criteria: ', array( 'data' => $search_criteria ) );
723
724 31
		unset( $get );
725
726 31
		return $search_criteria;
727
	}
728
729
	/**
730
	 * Filters the \GF_Query with advanced logic.
731
	 *
732
	 * Dropin for the legacy flat filters when \GF_Query is available.
733
	 *
734
	 * @param \GF_Query $query The current query object reference
735
	 * @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...
736
	 * @param \GV\Request $request The request object
737
	 */
738 52
	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...
739
		/**
740
		 * This is a shortcut to get all the needed search criteria.
741
		 * We feed these into an new GF_Query and tack them onto the current object.
742
		 */
743 52
		$search_criteria = $this->filter_entries( array(), null, array( 'id' => $view->ID ), true /** force search_criteria */ );
744
745
		/**
746
		 * Call any userland filters that they might have.
747
		 */
748 52
		remove_filter( 'gravityview_fe_search_criteria', array( $this, 'filter_entries' ), 10, 3 );
749 52
		$search_criteria = apply_filters( 'gravityview_fe_search_criteria', $search_criteria, $view->form->ID, $view->settings->as_atts() );
750 52
		add_filter( 'gravityview_fe_search_criteria', array( $this, 'filter_entries' ), 10, 3 );
751
752 52
		$query_class = $view->get_query_class();
753
754 52
		if ( empty( $search_criteria['field_filters'] ) ) {
755 48
			return;
756
		}
757
758 12
		$widgets = $view->widgets->by_id( $this->widget_id );
759 12
		if ( $widgets->count() ) {
760 10
			$widgets = $widgets->all();
761 10
			$widget  = $widgets[0];
762
763 10
			$search_fields = json_decode( $widget->configuration->get( 'search_fields' ), true );
764
765 10
			foreach ( (array) $search_fields as $search_field ) {
766 10
				if ( 'created_by' === $search_field['field'] && 'input_text' === $search_field['input'] ) {
767 1
					$created_by_text_mode = true;
768
				}
769
			}
770
		}
771
772 12
		$extra_conditions = array();
773 12
		$mode = 'any';
774
775 12
		foreach ( $search_criteria['field_filters'] as &$filter ) {
776 12
			if ( ! is_array( $filter ) ) {
777 12
				if ( in_array( strtolower( $filter ), array( 'any', 'all' ) ) ) {
778 12
					$mode = $filter;
779
				}
780 12
				continue;
781
			}
782
783
			// Construct a manual query for unapproved statuses
784 9
			if ( 'is_approved' === $filter['key'] && in_array( \GravityView_Entry_Approval_Status::UNAPPROVED, (array) $filter['value'] ) ) {
785 2
				$_tmp_query       = new $query_class( $view->form->ID, array(
786 2
					'field_filters' => array(
787
						array(
788 2
							'operator' => 'in',
789 2
							'key'      => 'is_approved',
790 2
							'value'    => (array) $filter['value'],
791
						),
792
						array(
793
							'operator' => 'is',
794
							'key'      => 'is_approved',
795
							'value'    => '',
796
						),
797 2
						'mode' => 'any'
0 ignored issues
show
introduced by
Key specified for array entry; first entry has no key
Loading history...
798
					),
799
				) );
800 2
				$_tmp_query_parts = $_tmp_query->_introspect();
801
802 2
				$extra_conditions[] = $_tmp_query_parts['where'];
803
804 2
				$filter = false;
805 2
				continue;
806
			}
807
808
			// Construct manual query for text mode creator search
809 9
			if ( 'created_by' === $filter['key'] && ! empty( $created_by_text_mode ) ) {
810 1
				$extra_conditions[] = new GravityView_Widget_Search_Author_GF_Query_Condition( $filter, $view );
811 1
				$filter = false;
812 1
				continue;
813
			}
814
815
			// By default, we want searches to be wildcard for each field.
816 8
			$filter['operator'] = empty( $filter['operator'] ) ? 'contains' : $filter['operator'];
817
818
			// For multichoice, let's have an in (OR) search.
819 8
			if ( is_array( $filter['value'] ) ) {
820 3
				$filter['operator'] = 'in'; // @todo what about in contains (OR LIKE chains)?
821
			}
822
823
			// Default form with joins functionality
824 8
			if ( empty( $filter['form_id'] ) ) {
825 1
				$filter['form_id'] = $view->form ? $view->form->ID : 0;
826
			}
827
828
			/**
829
			 * @filter `gravityview_search_operator` Modify the search operator for the field (contains, is, isnot, etc)
830
			 * @param string $operator Existing search operator
831
			 * @param array $filter array with `key`, `value`, `operator`, `type` keys
832
			 * @since develop
833
			 * @param \GV\View $view The View we're operating on.
834
			 */
835 8
			$filter['operator'] = apply_filters( 'gravityview_search_operator', $filter['operator'], $filter, $view );
836
		}
837
838 12
		if ( ! empty( $search_criteria['start_date'] ) || ! empty( $search_criteria['end_date'] ) ) {
839 1
			$date_criteria = array();
840
841 1
			if ( isset( $search_criteria['start_date'] ) ) {
842 1
				$date_criteria['start_date'] = $search_criteria['start_date'];
843
			}
844
845 1
			if ( isset( $search_criteria['end_date'] ) ) {
846 1
				$date_criteria['end_date'] = $search_criteria['end_date'];
847
			}
848
849 1
			$_tmp_query         = new $query_class( $view->form->ID, $date_criteria );
850 1
			$_tmp_query_parts   = $_tmp_query->_introspect();
851 1
			$extra_conditions[] = $_tmp_query_parts['where'];
852
		}
853
854 12
		$search_conditions = array();
855
856 12
		if ( $filters = array_filter( $search_criteria['field_filters'] ) ) {
857
858 12
			foreach ( $filters as $filter ) {
859 12
				if ( ! is_array( $filter ) ) {
860 7
					continue;
861
				}
862
863
				/**
864
				 * Parse the filter criteria to generate the needed
865
				 * WHERE condition. This is a trick to not write our own generation
866
				 * code by reusing what's inside GF_Query already as they
867
				 * take care of many small things like forcing numeric, etc.
868
				 */
869 8
				$_tmp_query       = new $query_class( $filter['form_id'], array( 'mode' => 'any', 'field_filters' => array( $filter ) ) );
870 8
				$_tmp_query_parts = $_tmp_query->_introspect();
871 8
				$search_condition = $_tmp_query_parts['where'];
872
873 8
				if ( empty( $filter['key'] ) &&  $search_condition->expressions ) {
874
					 foreach ( $search_condition->expressions as $condition ) {
875
						$search_conditions[] = new GravityView_Widget_Search_All_GF_Query_Condition( $condition, $view );
876
					 }
877
				} else {
878 8
					$left = $search_condition->left;
879 8
					$alias = $query->_alias( $left->field_id, $left->source, $left->is_entry_column() ? 't' : 'm' );
880
881 8
					if ( $view->joins && $left->field_id == GF_Query_Column::META ) {
882
						$search_conditions[] = new GravityView_Widget_Search_All_GF_Query_Condition( $search_condition, $view );
883
					} else {
884 8
						$search_conditions[] = new GF_Query_Condition(
885 8
							new GF_Query_Column( $left->field_id, $left->source, $alias ),
886 8
							$search_condition->operator,
887 8
							$search_condition->right
888
						);
889
					}
890
				}
891
			}
892
893 12
			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...
894 8
				$search_conditions = array( call_user_func_array( '\GF_Query_Condition::' . ( $mode == 'all' ? '_and' : '_or' ), $search_conditions ) );
895
			}
896
		}
897
898
		/**
899
		 * Grab the current clauses. We'll be combining them shortly.
900
		 */
901 12
		$query_parts = $query->_introspect();
902
903
		/**
904
		 * Combine the parts as a new WHERE clause.
905
		 */
906 12
		$where = call_user_func_array( '\GF_Query_Condition::_and', array_merge( array( $query_parts['where'] ), $search_conditions, $extra_conditions ) );
907 12
		$query->where( $where );
908 12
	}
909
910
	/**
911
	 * Convert $_GET/$_POST key to the field/meta ID
912
	 *
913
	 * Examples:
914
	 * - `filter_is_starred` => `is_starred`
915
	 * - `filter_1_2` => `1.2`
916
	 * - `filter_5` => `5`
917
	 *
918
	 * @since 2.0
919
	 *
920
	 * @param string $key $_GET/_$_POST search key
921
	 *
922
	 * @return string
923
	 */
924 16
	private function convert_request_key_to_filter_key( $key ) {
925
926 16
		$field_id = str_replace( 'filter_', '', $key );
927
928
		// calculates field_id, removing 'filter_' and for '_' for advanced fields ( like name or checkbox )
929 16
		if ( preg_match('/^[0-9_]+$/ism', $field_id ) ) {
0 ignored issues
show
Coding Style introduced by
Expected 1 spaces after opening bracket; 0 found
Loading history...
930 14
			$field_id = str_replace( '_', '.', $field_id );
931
		}
932
933 16
		return $field_id;
934
	}
935
936
	/**
937
	 * Prepare the field filters to GFAPI
938
	 *
939
	 * 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.
940
	 *
941
	 * Format searched values
942
	 *
943
	 * @param  string $filter_key ID of the field, or entry meta key
944
	 * @param  string $value $_GET/$_POST search value
945
	 * @param  \GV\View $view The view we're looking at
946
	 * @param array[] $searchable_fields The searchable fields as configured by the widget.
947
	 * @param string[] $get The $_GET/$_POST array.
948
	 *
949
	 * @since develop Added 5th $get parameter for operator overrides.
950
	 * @todo Set function as private.
951
	 *
952
	 * @return array|false 1 or 2 deph levels, false if not allowed
953
	 */
954 16
	public function prepare_field_filter( $filter_key, $value, $view, $searchable_fields, $get = array() ) {
955 16
		$key = $filter_key;
956 16
		$filter_key = explode( ':', $filter_key ); // field_id, form_id
957
958 16
		$form = null;
959
960 16
		if ( count( $filter_key ) > 1 ) {
961
			// form is specified
962 1
			list( $field_id, $form_id ) = $filter_key;
963
964 1
			if ( $forms = \GV\View::get_joined_forms( $view->ID ) ) {
965 1
				if ( ! $form = \GV\GF_Form::by_id( $form_id ) ) {
966
					return false;
967
				}
968
			}
969
970
			// form is allowed
971 1
			$found = false;
972 1
			foreach ( $forms as $form ) {
973 1
				if ( $form->ID == $form_id ) {
974 1
					$found = true;
975 1
					break;
976
				}
977
			}
978
979 1
			if ( ! $found ) {
980
				return false;
981
			}
982
983
			// form is in searchable fields
984 1
			$found = false;
985 1
			foreach ( $searchable_fields as $field ) {
986 1
				if ( $field_id == $field['field'] && $form->ID == $field['form_id'] ) {
987 1
					$found = true;
988 1
					break;
989
				}
990
			}
991
992 1
			if ( ! $found ) {
993 1
				return false;
994
			}
995
		} else {
996 16
			$field_id = reset( $filter_key );
997 16
			$searchable_fields = wp_list_pluck( $searchable_fields, 'field' );
998 16
			if ( ! in_array( 'search_all', $searchable_fields ) && ! in_array( $field_id, $searchable_fields ) ) {
999 1
				return false;
1000
			}
1001
		}
1002
		
1003 15
		if ( ! $form ) {
1004
			// fallback
1005 15
			$form = $view->form;
1006
		}
1007
1008
		// get form field array
1009 15
		$form_field = is_numeric( $field_id ) ? \GV\GF_Field::by_id( $form, $field_id ) : \GV\Internal_Field::by_id( $field_id );
1010
1011
		// default filter array
1012
		$filter = array(
1013 15
			'key'   => $field_id,
1014 15
			'value' => $value,
1015 15
			'form_id' => $form->ID,
1016
		);
1017
1018 15
		switch ( $form_field->type ) {
1019
1020 15
			case 'select':
1021 15
			case 'radio':
1022 1
				$filter['operator'] = $this->get_operator( $get, $key, array( 'is' ), 'is' );
1023 1
				break;
1024
1025 14
			case 'post_category':
1026
1027
				if ( ! is_array( $value ) ) {
1028
					$value = array( $value );
1029
				}
1030
1031
				// Reset filter variable
1032
				$filter = array();
1033
1034
				foreach ( $value as $val ) {
1035
					$cat = get_term( $val, 'category' );
1036
					$filter[] = array(
1037
						'key'      => $field_id,
1038
						'value'    => esc_attr( $cat->name ) . ':' . $val,
1039
						'operator' => $this->get_operator( $get, $key, array( 'is' ), 'is' ),
1040
					);
1041
				}
1042
1043
				break;
1044
1045 14
			case 'multiselect':
1046
1047
				if ( ! is_array( $value ) ) {
1048
					break;
1049
				}
1050
1051
				// Reset filter variable
1052
				$filter = array();
1053
1054
				foreach ( $value as $val ) {
1055
					$filter[] = array( 'key' => $field_id, 'value' => $val );
1056
				}
1057
1058
				break;
1059
1060 14
			case 'checkbox':
1061
				// convert checkbox on/off into the correct search filter
1062
				if ( false !== strpos( $field_id, '.' ) && ! empty( $form_field->inputs ) && ! empty( $form_field->choices ) ) {
1063
					foreach ( $form_field->inputs as $k => $input ) {
1064
						if ( $input['id'] == $field_id ) {
1065
							$filter['value'] = $form_field->choices[ $k ]['value'];
1066
							$filter['operator'] = $this->get_operator( $get, $key, array( 'is' ), 'is' );
1067
							break;
1068
						}
1069
					}
1070
				} elseif ( is_array( $value ) ) {
1071
1072
					// Reset filter variable
1073
					$filter = array();
1074
1075
					foreach ( $value as $val ) {
1076
						$filter[] = array(
1077
							'key'      => $field_id,
1078
							'value'    => $val,
1079
							'operator' => $this->get_operator( $get, $key, array( 'is' ), 'is' ),
1080
						);
1081
					}
1082
				}
1083
1084
				break;
1085
1086 14
			case 'name':
1087 14
			case 'address':
1088
1089
				if ( false === strpos( $field_id, '.' ) ) {
1090
1091
					$words = explode( ' ', $value );
1092
1093
					$filters = array();
1094
					foreach ( $words as $word ) {
1095
						if ( ! empty( $word ) && strlen( $word ) > 1 ) {
1096
							// Keep the same key for each filter
1097
							$filter['value'] = $word;
1098
							// Add a search for the value
1099
							$filters[] = $filter;
1100
						}
1101
					}
1102
1103
					$filter = $filters;
1104
				}
1105
1106
				// State/Province should be exact matches
1107
				if ( 'address' === $form_field->field->type ) {
1108
1109
					$searchable_fields = $this->get_view_searchable_fields( $view, true );
1110
1111
					foreach ( $searchable_fields as $searchable_field ) {
1112
1113
						if( $form_field->ID !== $searchable_field['field'] ) {
1114
							continue;
1115
						}
1116
1117
						// Only exact-match dropdowns, not text search
1118
						if( in_array( $searchable_field['input'], array( 'text', 'search' ), true ) ) {
1119
							continue;
1120
						}
1121
1122
						$input_id = gravityview_get_input_id_from_id( $form_field->ID );
1123
1124
						if ( 4 === $input_id ) {
1125
							$filter['operator'] = $this->get_operator( $get, $key, array( 'is' ), 'is' );
1126
						};
1127
					}
1128
				}
1129
1130
				break;
1131
1132 14
			case 'date':
1133
1134 8
				$date_format = $this->get_datepicker_format( true );
1135
1136 8
				if ( is_array( $value ) ) {
1137
1138
					// Reset filter variable
1139
					$filter = array();
1140
1141
					foreach ( $value as $k => $date ) {
1142
						if ( empty( $date ) ) {
1143
							continue;
1144
						}
1145
						$operator = 'start' === $k ? '>=' : '<=';
1146
1147
						/**
1148
						 * @hack
1149
						 * @since 1.16.3
1150
						 * Safeguard until GF implements '<=' operator
1151
						 */
1152
						if( !GFFormsModel::is_valid_operator( $operator ) && $operator === '<=' ) {
0 ignored issues
show
introduced by
Expected 1 space after "!"; 0 found
Loading history...
1153
							$operator = '<';
1154
							$date = date( 'Y-m-d', strtotime( self::get_formatted_date( $date, 'Y-m-d', $date_format ) . ' +1 day' ) );
1155
						}
1156
1157
						$filter[] = array(
1158
							'key'      => $field_id,
1159
							'value'    => self::get_formatted_date( $date, 'Y-m-d', $date_format ),
1160
							'operator' => $this->get_operator( $get, $key, array( $operator ), $operator ),
1161
						);
1162
					}
1163
				} else {
1164 8
					$date = $value;
1165 8
					$filter['value'] = self::get_formatted_date( $date, 'Y-m-d', $date_format );
1166 8
					$filter['operator'] = $this->get_operator( $get, $key, array( 'is' ), 'is' );
1167
				}
1168
1169 8
				break;
1170
0 ignored issues
show
Coding Style introduced by
Functions must not contain multiple empty lines in a row; found 2 empty lines
Loading history...
1171
1172
		} // switch field type
1173
1174 15
		return $filter;
1175
	}
1176
1177
	/**
1178
	 * Get the Field Format form GravityForms
1179
	 *
1180
	 * @param GF_Field_Date $field The field object
1181
	 * @since 1.10
1182
	 *
1183
	 * @return string Format of the date in the database
1184
	 */
1185
	public static function get_date_field_format( GF_Field_Date $field ) {
1186
		$format = 'm/d/Y';
1187
		$datepicker = array(
1188
			'mdy' => 'm/d/Y',
1189
			'dmy' => 'd/m/Y',
1190
			'dmy_dash' => 'd-m-Y',
1191
			'dmy_dot' => 'd.m.Y',
1192
			'ymd_slash' => 'Y/m/d',
1193
			'ymd_dash' => 'Y-m-d',
1194
			'ymd_dot' => 'Y.m.d',
1195
		);
1196
1197
		if ( ! empty( $field->dateFormat ) && isset( $datepicker[ $field->dateFormat ] ) ){
1198
			$format = $datepicker[ $field->dateFormat ];
1199
		}
1200
1201
		return $format;
1202
	}
1203
1204
	/**
1205
	 * Format a date value
1206
	 *
1207
	 * @param string $value Date value input
1208
	 * @param string $format Wanted formatted date
1209
	 *
1210
	 * @since 2.1.2
1211
	 * @param string $value_format The value format. Default: Y-m-d
1212
	 *
1213
	 * @return string
1214
	 */
1215 8
	public static function get_formatted_date( $value = '', $format = 'Y-m-d', $value_format = 'Y-m-d' ) {
1216
1217 8
		$date = date_create_from_format( $value_format, $value );
1218
1219 8
		if ( empty( $date ) ) {
1220
			gravityview()->log->debug( 'Date format not valid: {value}', array( 'value' => $value ) );
1221
			return '';
1222
		}
1223 8
		return $date->format( $format );
1224
	}
1225
1226
1227
	/**
1228
	 * Include this extension templates path
1229
	 * @param array $file_paths List of template paths ordered
1230
	 */
1231 1
	public function add_template_path( $file_paths ) {
1232
1233
		// Index 100 is the default GravityView template path.
1234 1
		$file_paths[102] = self::$file . 'templates/';
1235
1236 1
		return $file_paths;
1237
	}
1238
1239
	/**
1240
	 * Check whether the configured search fields have a date field
1241
	 *
1242
	 * @since 1.17.5
1243
	 *
1244
	 * @param array $search_fields
1245
	 *
1246
	 * @return bool True: has a `date` or `date_range` field
1247
	 */
1248 4
	private function has_date_field( $search_fields ) {
1249
1250 4
		$has_date = false;
1251
1252 4
		foreach ( $search_fields as $k => $field ) {
1253 4
			if ( in_array( $field['input'], array( 'date', 'date_range', 'entry_date' ) ) ) {
1254
				$has_date = true;
1255
				break;
1256
			}
1257
		}
1258
1259 4
		return $has_date;
1260
	}
1261
1262
	/**
1263
	 * Renders the Search Widget
1264
	 * @param array $widget_args
1265
	 * @param string $content
1266
	 * @param string $context
1267
	 *
1268
	 * @return void
1269
	 */
1270 4
	public function render_frontend( $widget_args, $content = '', $context = '' ) {
1271
		/** @var GravityView_View $gravityview_view */
1272 4
		$gravityview_view = GravityView_View::getInstance();
1273
1274 4
		if ( empty( $gravityview_view ) ) {
1275
			gravityview()->log->debug( '$gravityview_view not instantiated yet.' );
1276
			return;
1277
		}
1278
1279
		// get configured search fields
1280 4
		$search_fields = ! empty( $widget_args['search_fields'] ) ? json_decode( $widget_args['search_fields'], true ) : '';
1281
1282 4
		if ( empty( $search_fields ) || ! is_array( $search_fields ) ) {
1283
			gravityview()->log->debug( 'No search fields configured for widget:', array( 'data' => $widget_args ) );
1284
			return;
1285
		}
1286
1287 4
		$view = \GV\View::by_id( $gravityview_view->view_id );
1288
1289
		// prepare fields
1290 4
		foreach ( $search_fields as $k => $field ) {
1291
1292 4
			$updated_field = $field;
1293
1294 4
			$updated_field = $this->get_search_filter_details( $updated_field );
1295
1296 4
			switch ( $field['field'] ) {
1297
1298 4
				case 'search_all':
1299 4
					$updated_field['key'] = 'search_all';
1300 4
					$updated_field['input'] = 'search_all';
1301 4
					$updated_field['value'] = $this->rgget_or_rgpost( 'gv_search' );
1302 4
					break;
1303
1304
				case 'entry_date':
1305
					$updated_field['key'] = 'entry_date';
1306
					$updated_field['input'] = 'entry_date';
1307
					$updated_field['value'] = array(
1308
						'start' => $this->rgget_or_rgpost( 'gv_start' ),
1309
						'end' => $this->rgget_or_rgpost( 'gv_end' ),
1310
					);
1311
					break;
1312
1313
				case 'entry_id':
1314
					$updated_field['key'] = 'entry_id';
1315
					$updated_field['input'] = 'entry_id';
1316
					$updated_field['value'] = $this->rgget_or_rgpost( 'gv_id' );
1317
					break;
1318
1319
				case 'created_by':
1320
					$updated_field['key'] = 'created_by';
1321
					$updated_field['name'] = 'gv_by';
1322
					$updated_field['value'] = $this->rgget_or_rgpost( 'gv_by' );
1323
					$updated_field['choices'] = self::get_created_by_choices( $view );
1324
					break;
1325
				
1326
				case 'is_approved':
1327
					$updated_field['key'] = 'is_approved';
1328
					$updated_field['value'] = $this->rgget_or_rgpost( 'filter_is_approved' );
1329
					$updated_field['choices'] = self::get_is_approved_choices();
1330
					break;
1331
			}
1332
1333 4
			$search_fields[ $k ] = $updated_field;
1334
		}
1335
1336 4
		gravityview()->log->debug( 'Calculated Search Fields: ', array( 'data' => $search_fields ) );
1337
1338
		/**
1339
		 * @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.
1340
		 * @param array $search_fields Array of search filters with `key`, `label`, `value`, `type`, `choices` keys
1341
		 * @param GravityView_Widget_Search $this Current widget object
1342
		 * @param array $widget_args Args passed to this method. {@since 1.8}
1343
		 * @param \GV\Template_Context $context {@since 2.0}
1344
		 * @var array
1345
		 */
1346 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...
1347
1348 4
		$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...
1349
1350
		/** @since 1.14 */
1351 4
		$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...
1352
1353 4
		$custom_class = ! empty( $widget_args['custom_class'] ) ? $widget_args['custom_class'] : '';
1354
1355 4
		$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...
1356
1357 4
		$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...
1358
1359 4
		if ( $this->has_date_field( $search_fields ) ) {
1360
			// enqueue datepicker stuff only if needed!
1361
			$this->enqueue_datepicker();
1362
		}
1363
1364 4
		$this->maybe_enqueue_flexibility();
1365
1366 4
		$gravityview_view->render( 'widget', 'search', false );
1367 4
	}
1368
1369
	/**
1370
	 * Get the search class for a search form
1371
	 *
1372
	 * @since 1.5.4
1373
	 *
1374
	 * @return string Sanitized CSS class for the search form
1375
	 */
1376 4
	public static function get_search_class( $custom_class = '' ) {
1377 4
		$gravityview_view = GravityView_View::getInstance();
1378
1379 4
		$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...
1380
1381 4
		if ( ! empty( $custom_class )  ) {
1382
			$search_class .= ' '.$custom_class;
1383
		}
1384
1385
		/**
1386
		 * @filter `gravityview_search_class` Modify the CSS class for the search form
1387
		 * @param string $search_class The CSS class for the search form
1388
		 */
1389 4
		$search_class = apply_filters( 'gravityview_search_class', $search_class );
1390
1391
		// Is there an active search being performed? Used by fe-views.js
1392 4
		$search_class .= GravityView_frontend::getInstance()->isSearch() ? ' gv-is-search' : '';
1393
1394 4
		return gravityview_sanitize_html_class( $search_class );
1395
	}
1396
1397
1398
	/**
1399
	 * Calculate the search form action
1400
	 * @since 1.6
1401
	 *
1402
	 * @return string
1403
	 */
1404 4
	public static function get_search_form_action() {
1405 4
		$gravityview_view = GravityView_View::getInstance();
1406
1407 4
		$post_id = $gravityview_view->getPostId() ? $gravityview_view->getPostId() : $gravityview_view->getViewId();
1408
1409 4
		$url = add_query_arg( array(), get_permalink( $post_id ) );
1410
1411 4
		return esc_url( $url );
1412
	}
1413
1414
	/**
1415
	 * Get the label for a search form field
1416
	 * @param  array $field      Field setting as sent by the GV configuration - has `field`, `input` (input type), and `label` keys
1417
	 * @param  array $form_field Form field data, as fetched by `gravityview_get_field()`
1418
	 * @return string             Label for the search form
1419
	 */
1420 4
	private static function get_field_label( $field, $form_field = array() ) {
1421
1422 4
		$label = \GV\Utils::_GET( 'label', \GV\Utils::get( $field, 'label' ) );
1423
1424 4
		if ( ! $label ) {
1425
1426 4
			$label = isset( $form_field['label'] ) ? $form_field['label'] : '';
1427
1428 4
			switch( $field['field'] ) {
1429 4
				case 'search_all':
1430 4
					$label = __( 'Search Entries:', 'gravityview' );
1431 4
					break;
1432
				case 'entry_date':
1433
					$label = __( 'Filter by date:', 'gravityview' );
1434
					break;
1435
				case 'entry_id':
1436
					$label = __( 'Entry ID:', 'gravityview' );
1437
					break;
1438
				default:
1439
					// If this is a field input, not a field
1440
					if ( strpos( $field['field'], '.' ) > 0 && ! empty( $form_field['inputs'] ) ) {
1441
1442
						// Get the label for the field in question, which returns an array
1443
						$items = wp_list_filter( $form_field['inputs'], array( 'id' => $field['field'] ) );
1444
1445
						// Get the item with the `label` key
1446
						$values = wp_list_pluck( $items, 'label' );
1447
1448
						// There will only one item in the array, but this is easier
1449
						foreach ( $values as $value ) {
1450
							$label = $value;
1451
							break;
1452
						}
1453
					}
1454
			}
1455
		}
1456
1457
		/**
1458
		 * @filter `gravityview_search_field_label` Modify the label for a search field. Supports returning HTML
1459
		 * @since 1.17.3 Added $field parameter
1460
		 * @param[in,out] string $label Existing label text, sanitized.
1461
		 * @param[in] array $form_field Gravity Forms field array, as returned by `GFFormsModel::get_field()`
1462
		 * @param[in] array $field Field setting as sent by the GV configuration - has `field`, `input` (input type), and `label` keys
1463
		 */
1464 4
		$label = apply_filters( 'gravityview_search_field_label', esc_attr( $label ), $form_field, $field );
1465
1466 4
		return $label;
1467
	}
1468
1469
	/**
1470
	 * Prepare search fields to frontend render with other details (label, field type, searched values)
1471
	 *
1472
	 * @param array $field
1473
	 * @return array
1474
	 */
1475 4
	private function get_search_filter_details( $field ) {
1476
1477 4
		$gravityview_view = GravityView_View::getInstance();
1478
1479 4
		$form = $gravityview_view->getForm();
1480
1481
		// for advanced field ids (eg, first name / last name )
1482 4
		$name = 'filter_' . str_replace( '.', '_', $field['field'] );
1483
1484
		// get searched value from $_GET/$_POST (string or array)
1485 4
		$value = $this->rgget_or_rgpost( $name );
1486
1487
		// get form field details
1488 4
		$form_field = gravityview_get_field( $form, $field['field'] );
1489
1490
		$filter = array(
1491 4
			'key' => $field['field'],
1492 4
			'name' => $name,
1493 4
			'label' => self::get_field_label( $field, $form_field ),
1494 4
			'input' => $field['input'],
1495 4
			'value' => $value,
1496 4
			'type' => $form_field['type'],
1497
		);
1498
1499
		// collect choices
1500 4
		if ( 'post_category' === $form_field['type'] && ! empty( $form_field['displayAllCategories'] ) && empty( $form_field['choices'] ) ) {
1501
			$filter['choices'] = gravityview_get_terms_choices();
1502 4
		} elseif ( ! empty( $form_field['choices'] ) ) {
1503
			$filter['choices'] = $form_field['choices'];
1504
		}
1505
1506 4
		if ( 'date_range' === $field['input'] && empty( $value ) ) {
1507
			$filter['value'] = array( 'start' => '', 'end' => '' );
1508
		}
1509
1510 4
		return $filter;
1511
1512
	}
1513
1514
	/**
1515
	 * Calculate the search choices for the users
1516
	 *
1517
	 * @param \GV\View $view The view
1518
	 * @since develop
1519
	 *
1520
	 * @since 1.8
1521
	 *
1522
	 * @return array Array of user choices (value = ID, text = display name)
1523
	 */
1524
	private static function get_created_by_choices( $view ) {
1525
1526
		/**
1527
		 * filter gravityview/get_users/search_widget
1528
		 * @see \GVCommon::get_users
1529
		 */
1530
		$users = GVCommon::get_users( 'search_widget', array( 'fields' => array( 'ID', 'display_name' ) ) );
1531
1532
		$choices = array();
1533
		foreach ( $users as $user ) {
1534
			/**
1535
			 * @filter `gravityview/search/created_by/text` Filter the display text in created by search choices
1536
			 * @since develop
1537
			 * @param string[in,out] The text. Default: $user->display_name
1538
			 * @param \WP_User $user The user.
1539
			 * @param \GV\View $view The view.
1540
			 */
1541
			$text = apply_filters( 'gravityview/search/created_by/text', $user->display_name, $user, $view );
1542
			$choices[] = array(
1543
				'value' => $user->ID,
1544
				'text' => $text,
1545
			);
1546
		}
1547
1548
		return $choices;
1549
	}
1550
1551
	/**
1552
	 * Calculate the search checkbox choices for approval status
1553
	 *
1554
	 * @since develop
1555
	 *
1556
	 * @return array Array of approval status choices (value = status, text = display name)
1557
	 */
1558
	private static function get_is_approved_choices() {
1559
1560
		$choices = array();
1561
		foreach ( GravityView_Entry_Approval_Status::get_all() as $status ) {
1562
			$choices[] = array(
1563
				'value' => $status['value'],
1564
				'text' => $status['label'],
1565
			);
1566
		}
1567
1568
		return $choices;
1569
	}
1570
1571
	/**
1572
	 * Output the Clear Search Results button
1573
	 * @since 1.5.4
1574
	 */
1575 4
	public static function the_clear_search_button() {
1576 4
		$gravityview_view = GravityView_View::getInstance();
1577
1578 4
		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...
1579
1580
			$url = strtok( add_query_arg( array() ), '?' );
1581
1582
			echo gravityview_get_link( $url, esc_html__( 'Clear', 'gravityview' ), 'class=button gv-search-clear' );
0 ignored issues
show
introduced by
Expected a sanitizing function (see Codex for 'Data Validation'), but instead saw 'gravityview_get_link'
Loading history...
1583
1584
		}
1585 4
	}
1586
1587
	/**
1588
	 * Based on the search method, fetch the value for a specific key
1589
	 *
1590
	 * @since 1.16.4
1591
	 *
1592
	 * @param string $name Name of the request key to fetch the value for
1593
	 *
1594
	 * @return mixed|string Value of request at $name key. Empty string if empty.
1595
	 */
1596 4
	private function rgget_or_rgpost( $name ) {
1597 4
		$value = \GV\Utils::_REQUEST( $name );
1598
1599 4
		$value = stripslashes_deep( $value );
1600
1601 4
		$value = gv_map_deep( $value, 'rawurldecode' );
1602
1603 4
		$value = gv_map_deep( $value, '_wp_specialchars' );
1604
1605 4
		return $value;
1606
	}
1607
1608
1609
	/**
1610
	 * Require the datepicker script for the frontend GV script
1611
	 * @param array $js_dependencies Array of existing required scripts for the fe-views.js script
1612
	 * @return array Array required scripts, with `jquery-ui-datepicker` added
1613
	 */
1614
	public function add_datepicker_js_dependency( $js_dependencies ) {
1615
1616
		$js_dependencies[] = 'jquery-ui-datepicker';
1617
1618
		return $js_dependencies;
1619
	}
1620
1621
	/**
1622
	 * Modify the array passed to wp_localize_script()
1623
	 *
1624
	 * @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...
1625
	 * @param array $view_data View data array with View settings
1626
	 *
1627
	 * @return array
1628
	 */
1629
	public function add_datepicker_localization( $localizations = array(), $view_data = array() ) {
1630
		global $wp_locale;
0 ignored issues
show
Compatibility Best Practice introduced by
Use of global functionality is not recommended; it makes your code harder to test, and less reusable.

Instead of relying on global state, we recommend one of these alternatives:

1. Pass all data via parameters

function myFunction($a, $b) {
    // Do something
}

2. Create a class that maintains your state

class MyClass {
    private $a;
    private $b;

    public function __construct($a, $b) {
        $this->a = $a;
        $this->b = $b;
    }

    public function myFunction() {
        // Do something
    }
}
Loading history...
1631
1632
		/**
1633
		 * @filter `gravityview_datepicker_settings` Modify the datepicker settings
1634
		 * @see http://api.jqueryui.com/datepicker/ Learn what settings are available
1635
		 * @see http://www.renegadetechconsulting.com/tutorials/jquery-datepicker-and-wordpress-i18n Thanks for the helpful information on $wp_locale
1636
		 * @param array $js_localization The data padded to the Javascript file
1637
		 * @param array $view_data View data array with View settings
1638
		 */
1639
		$datepicker_settings = apply_filters( 'gravityview_datepicker_settings', array(
1640
			'yearRange' => '-5:+5',
1641
			'changeMonth' => true,
1642
			'changeYear' => true,
1643
			'closeText' => esc_attr_x( 'Close', 'Close calendar', 'gravityview' ),
1644
			'prevText' => esc_attr_x( 'Prev', 'Previous month in calendar', 'gravityview' ),
1645
			'nextText' => esc_attr_x( 'Next', 'Next month in calendar', 'gravityview' ),
1646
			'currentText' => esc_attr_x( 'Today', 'Today in calendar', 'gravityview' ),
1647
			'weekHeader' => esc_attr_x( 'Week', 'Week in calendar', 'gravityview' ),
1648
			'monthStatus'       => __( 'Show a different month', 'gravityview' ),
1649
			'monthNames'        => array_values( $wp_locale->month ),
1650
			'monthNamesShort'   => array_values( $wp_locale->month_abbrev ),
1651
			'dayNames'          => array_values( $wp_locale->weekday ),
1652
			'dayNamesShort'     => array_values( $wp_locale->weekday_abbrev ),
1653
			'dayNamesMin'       => array_values( $wp_locale->weekday_initial ),
1654
			// get the start of week from WP general setting
1655
			'firstDay'          => get_option( 'start_of_week' ),
1656
			// is Right to left language? default is false
1657
			'isRTL'             => is_rtl(),
1658
		), $view_data );
1659
1660
		$localizations['datepicker'] = $datepicker_settings;
1661
1662
		return $localizations;
1663
1664
	}
1665
1666
	/**
1667
	 * Register search widget scripts, including Flexibility
1668
	 *
1669
	 * @see https://github.com/10up/flexibility
1670
	 *
1671
	 * @since 1.17
1672
	 *
1673
	 * @return void
1674
	 */
1675
	public function register_scripts() {
1676
		wp_register_script( 'gv-flexibility', plugins_url( 'assets/lib/flexibility/flexibility.js', GRAVITYVIEW_FILE ), array(), \GV\Plugin::$version, true );
1677
	}
1678
1679
	/**
1680
	 * If the current visitor is running IE 8 or 9, enqueue Flexibility
1681
	 *
1682
	 * @since 1.17
1683
	 *
1684
	 * @return void
1685
	 */
1686 4
	private function maybe_enqueue_flexibility() {
1687 4
		if ( isset( $_SERVER['HTTP_USER_AGENT'] ) && preg_match( '/MSIE [8-9]/', $_SERVER['HTTP_USER_AGENT'] ) ) {
0 ignored issues
show
introduced by
Due to using Batcache, server side based client related logic will not work, use JS instead.
Loading history...
introduced by
Detected usage of a non-sanitized input variable: $_SERVER
Loading history...
1688
			wp_enqueue_script( 'gv-flexibility' );
1689
		}
1690 4
	}
1691
1692
	/**
1693
	 * Enqueue the datepicker script
1694
	 *
1695
	 * It sets the $gravityview->datepicker_class parameter
1696
	 *
1697
	 * @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.
1698
	 * @return void
1699
	 */
1700
	public function enqueue_datepicker() {
1701
		$gravityview_view = GravityView_View::getInstance();
1702
1703
		wp_enqueue_script( 'jquery-ui-datepicker' );
1704
1705
		add_filter( 'gravityview_js_dependencies', array( $this, 'add_datepicker_js_dependency' ) );
1706
		add_filter( 'gravityview_js_localization', array( $this, 'add_datepicker_localization' ), 10, 2 );
1707
1708
		$scheme = is_ssl() ? 'https://' : 'http://';
1709
		wp_enqueue_style( 'jquery-ui-datepicker', $scheme.'ajax.googleapis.com/ajax/libs/jqueryui/1.8.18/themes/smoothness/jquery-ui.css' );
1710
1711
		/**
1712
		 * @filter `gravityview_search_datepicker_class`
1713
		 * 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.
1714
		 * @param string $css_class CSS class to use. Default: `gv-datepicker datepicker mdy` \n
1715
		 * Options are:
1716
		 * - `mdy` (mm/dd/yyyy)
1717
		 * - `dmy` (dd/mm/yyyy)
1718
		 * - `dmy_dash` (dd-mm-yyyy)
1719
		 * - `dmy_dot` (dd.mm.yyyy)
1720
		 * - `ymd_slash` (yyyy/mm/dd)
1721
		 * - `ymd_dash` (yyyy-mm-dd)
1722
		 * - `ymd_dot` (yyyy.mm.dd)
1723
		 */
1724
		$datepicker_class = apply_filters( 'gravityview_search_datepicker_class', "gv-datepicker datepicker " . $this->get_datepicker_format() );
0 ignored issues
show
Coding Style Comprehensibility introduced by
The string literal gv-datepicker datepicker does not require double quotes, as per coding-style, please use single quotes.

PHP provides two ways to mark string literals. Either with single quotes 'literal' or with double quotes "literal". The difference between these is that string literals in double quotes may contain variables with are evaluated at run-time as well as escape sequences.

String literals in single quotes on the other hand are evaluated very literally and the only two characters that needs escaping in the literal are the single quote itself (\') and the backslash (\\). Every other character is displayed as is.

Double quoted string literals may contain other variables or more complex escape sequences.

<?php

$singleQuoted = 'Value';
$doubleQuoted = "\tSingle is $singleQuoted";

print $doubleQuoted;

will print an indented: Single is Value

If your string literal does not contain variables or escape sequences, it should be defined using single quotes to make that fact clear.

For more information on PHP string literals and available escape sequences see the PHP core documentation.

Loading history...
1725
1726
		$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...
1727
	}
1728
1729
	/**
1730
	 * Retrieve the datepicker format.
1731
	 *
1732
	 * @param bool $date_format Whether to return the PHP date format or the datpicker class name. Default: false.
1733
	 *
1734
	 * @see https://docs.gravityview.co/article/115-changing-the-format-of-the-search-widgets-date-picker
1735
	 *
1736
	 * @return string The datepicker format placeholder, or the PHP date format.
1737
	 */
1738 19
	private function get_datepicker_format( $date_format = false ) {
1739
1740 19
		$default_format = 'mdy';
1741
1742
		/**
1743
		 * @filter `gravityview/widgets/search/datepicker/format`
1744
		 * @since 2.1.1
1745
		 * @param string           $format Default: mdy
1746
		 * Options are:
1747
		 * - `mdy` (mm/dd/yyyy)
1748
		 * - `dmy` (dd/mm/yyyy)
1749
		 * - `dmy_dash` (dd-mm-yyyy)
1750
		 * - `dmy_dot` (dd.mm.yyyy)
1751
		 * - `ymd_slash` (yyyy/mm/dd)
1752
		 * - `ymd_dash` (yyyy-mm-dd)
1753
		 * - `ymd_dot` (yyyy.mm.dd)
1754
		 */
1755 19
		$format = apply_filters( 'gravityview/widgets/search/datepicker/format', $default_format );
1756
1757
		$gf_date_formats = array(
1758 19
			'mdy' => 'm/d/Y',
1759
1760
			'dmy_dash' => 'd-m-Y',
1761
			'dmy_dot' => 'd.m.Y',
1762
			'dmy' => 'd/m/Y',
1763
1764
			'ymd_slash' => 'Y/m/d',
1765
			'ymd_dash' => 'Y-m-d',
1766
			'ymd_dot' => 'Y.m.d',
1767
		);
1768
1769 19
		if ( ! $date_format ) {
1770
			// If the format key isn't valid, return default format key
1771
			return isset( $gf_date_formats[ $format ] ) ? $format : $default_format;
1772
		}
1773
1774
		// If the format key isn't valid, return default format value
1775 19
		return \GV\Utils::get( $gf_date_formats, $format, $gf_date_formats[ $default_format ] );
1776
	}
1777
1778
	/**
1779
	 * If previewing a View or page with embedded Views, make the search work properly by adding hidden fields with query vars
1780
	 *
1781
	 * @since 2.2.1
1782
	 *
1783
	 * @return void
1784
	 */
1785 4
	public function add_preview_inputs() {
1786 4
		global $wp;
0 ignored issues
show
Compatibility Best Practice introduced by
Use of global functionality is not recommended; it makes your code harder to test, and less reusable.

Instead of relying on global state, we recommend one of these alternatives:

1. Pass all data via parameters

function myFunction($a, $b) {
    // Do something
}

2. Create a class that maintains your state

class MyClass {
    private $a;
    private $b;

    public function __construct($a, $b) {
        $this->a = $a;
        $this->b = $b;
    }

    public function myFunction() {
        // Do something
    }
}
Loading history...
1787
1788 4
		if ( ! is_preview() || ! current_user_can( 'publish_gravityviews') ) {
0 ignored issues
show
Coding Style introduced by
Expected 1 spaces before closing bracket; 0 found
Loading history...
1789 4
			return;
1790
		}
1791
1792
		// Outputs `preview` and `post_id` variables
0 ignored issues
show
Unused Code Comprehensibility introduced by
36% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
1793
		foreach ( $wp->query_vars as $key => $value ) {
1794
			printf( '<input type="hidden" name="%s" value="%s" />', esc_attr( $key ), esc_attr( $value ) );
1795
		}
1796
1797
	}
1798
1799
	/**
1800
	 * Get an operator URL override.
1801
	 *
1802
	 * @param array  $get     Where to look for the operator.
1803
	 * @param string $key     The filter key to look for.
1804
	 * @param array  $allowed The allowed operators (whitelist).
1805
	 * @param string $default The default operator.
1806
	 *
1807
	 * @return string The operator.
1808
	 */
1809 18
	private function get_operator( $get, $key, $allowed, $default ) {
1810 18
		$operator = \GV\Utils::get( $get, "$key|op", $default );
1811
1812
		/**
1813
		 * @filter `gravityview/search/operator_whitelist` An array of allowed operators for a field.
1814
		 * @param[in,out] string[] A whitelist of allowed operators.
1815
		 * @param string The filter name.
1816
		 */
1817 18
		$allowed = apply_filters( 'gravityview/search/operator_whitelist', $allowed, $key );
1818
1819 18
		if ( ! in_array( $operator, $allowed, true ) ) {
1820 1
			$operator = $default;
1821
		}
1822
1823 18
		return $operator;
1824
	}
1825
1826
1827
} // end class
1828
1829
new GravityView_Widget_Search;
1830
1831
if ( ! gravityview()->plugin->supports( \GV\Plugin::FEATURE_GFQUERY ) ) {
1832
	return;
1833
}
1834
1835
/**
1836
 * A GF_Query condition that allows user data searches.
1837
 */
1838
class GravityView_Widget_Search_Author_GF_Query_Condition extends \GF_Query_Condition {
1839 1
	public function __construct( $filter, $view ) {
1840 1
		$this->value = $filter['value'];
1841 1
		$this->view = $view;
1842 1
	}
1843
1844 1
	public function sql( $query ) {
1845 1
		global $wpdb;
0 ignored issues
show
Compatibility Best Practice introduced by
Use of global functionality is not recommended; it makes your code harder to test, and less reusable.

Instead of relying on global state, we recommend one of these alternatives:

1. Pass all data via parameters

function myFunction($a, $b) {
    // Do something
}

2. Create a class that maintains your state

class MyClass {
    private $a;
    private $b;

    public function __construct($a, $b) {
        $this->a = $a;
        $this->b = $b;
    }

    public function myFunction() {
        // Do something
    }
}
Loading history...
1846
1847
		$user_meta_fields = array(
1848 1
			'nickname', 'first_name', 'last_name',
1849
		);
1850
1851
		/**
1852
		 * @filter `gravityview/widgets/search/created_by/user_meta_fields` Filter the user meta fields to search.
1853
		 * @param[in,out] array The user meta fields.
1854
		 * @param \GV\View $view The view.
1855
		 */
1856 1
		$user_meta_fields = apply_filters( 'gravityview/widgets/search/created_by/user_meta_fields', $user_meta_fields, $this->view );
1857
1858
		$user_fields = array(
1859 1
			'user_nicename', 'user_login', 'display_name', 'user_email', 
1860
		);
1861
1862
		/**
1863
		 * @filter `gravityview/widgets/search/created_by/user_fields` Filter the user fields to search.
1864
		 * @param[in,out] array The user fields.
1865
		 * @param \GV\View $view The view.
1866
		 */
1867 1
		$user_fields = apply_filters( 'gravityview/widgets/search/created_by/user_fields', $user_fields, $this->view );
1868
1869 1
		$conditions = array();
1870
1871 1
		foreach ( $user_fields as $user_field ) {
1872 1
			$conditions[] = $wpdb->prepare( "`u`.`$user_field` LIKE %s", '%' . $wpdb->esc_like( $this->value ) .  '%' );
1873
		}
1874
1875 1
		foreach ( $user_meta_fields as $meta_field ) {
1876 1
			$conditions[] = $wpdb->prepare( "(`um`.`meta_key` = %s AND `um`.`meta_value` LIKE %s)", $meta_field, '%' . $wpdb->esc_like( $this->value ) .  '%' );
0 ignored issues
show
Coding Style Comprehensibility introduced by
The string literal (`um`.`meta_key` = %s AN...`.`meta_value` LIKE %s) does not require double quotes, as per coding-style, please use single quotes.

PHP provides two ways to mark string literals. Either with single quotes 'literal' or with double quotes "literal". The difference between these is that string literals in double quotes may contain variables with are evaluated at run-time as well as escape sequences.

String literals in single quotes on the other hand are evaluated very literally and the only two characters that needs escaping in the literal are the single quote itself (\') and the backslash (\\). Every other character is displayed as is.

Double quoted string literals may contain other variables or more complex escape sequences.

<?php

$singleQuoted = 'Value';
$doubleQuoted = "\tSingle is $singleQuoted";

print $doubleQuoted;

will print an indented: Single is Value

If your string literal does not contain variables or escape sequences, it should be defined using single quotes to make that fact clear.

For more information on PHP string literals and available escape sequences see the PHP core documentation.

Loading history...
1877
		}
1878
1879 1
		$conditions = '(' . implode( ' OR ', $conditions ) . ')';
1880
1881 1
		$alias = $query->_alias( null );
1882
1883 1
		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)))";
0 ignored issues
show
introduced by
Usage of users/usermeta tables is highly discouraged in VIP context, For storing user additional user metadata, you should look at User Attributes.
Loading history...
1884
	}
1885
}
1886
1887
/**
1888
 * A GF_Query condition that allows searching across all fields.
1889
 */
1890
class GravityView_Widget_Search_All_GF_Query_Condition extends \GF_Query_Condition {
1891
	public function __construct( $search_condition, $view ) {
1892
		$this->search_condition = $search_condition;
1893
		$this->view = $view;
1894
	}
1895
1896
	public function sql( $query ) {
1897
		// @todo Search limit to only known fields
1898
		$parameters = $query->_introspect();
1899
1900
		// @todo We can search by properties as well in the future
1901
		$table = GFFormsModel::get_entry_meta_table_name();
1902
1903
		$conditions = array();
1904
1905
		foreach ( $parameters['aliases'] as $key => $alias ) {
1906
			if ( 'm' == $alias[0] && preg_match( '#\d+_\d+#', $key ) ) {
1907
				$conditions[] = sprintf( "EXISTS(SELECT * FROM `$table` WHERE `meta_value` %s %s AND `entry_id` = `%s`.`entry_id`)",
1908
					$this->search_condition->operator, $this->search_condition->right->sql( $query ), $alias );
0 ignored issues
show
Coding Style introduced by
This line of the multi-line function call does not seem to be indented correctly. Expected 16 spaces, but found 20.
Loading history...
1909
			}
1910
		}
1911
1912
		if ( $conditions ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $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...
1913
			return '(' . implode( ' OR ', $conditions ) . ')';
1914
		}
1915
1916
		return '';
1917
	}
1918
}
1919