Completed
Push — develop ( e73eb4...52a281 )
by Gennady
16:37
created

GravityView_Widget_Search::get_operator()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 16

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 2

Importance

Changes 0
Metric Value
cc 2
nc 2
nop 4
dl 0
loc 16
ccs 2
cts 2
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 33
	public function __construct() {
31
32 33
		$this->widget_id = 'search_bar';
33 33
		$this->widget_description = esc_html__( 'Search form for searching entries.', 'gravityview' );
34
35 33
		self::$instance = &$this;
36
37 33
		self::$file = plugin_dir_path( __FILE__ );
38
39 33
		$default_values = array( 'header' => 0, 'footer' => 0 );
40
41
		$settings = array(
42 33
			'search_layout' => array(
43 33
				'type' => 'radio',
44
				'full_width' => true,
45 33
				'label' => esc_html__( 'Search Layout', 'gravityview' ),
46 33
				'value' => 'horizontal',
47
				'options' => array(
48 33
					'horizontal' => esc_html__( 'Horizontal', 'gravityview' ),
49 33
					'vertical' => esc_html__( 'Vertical', 'gravityview' ),
50
				),
51
			),
52
			'search_clear' => array(
53 33
				'type' => 'checkbox',
54 33
				'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 33
				'type' => 'radio',
65
				'full_width' => true,
66 33
				'label' => esc_html__( 'Search Mode', 'gravityview' ),
67 33
				'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 33
				'value' => 'any',
69 33
				'class' => 'hide-if-js',
70
				'options' => array(
71 33
					'any' => esc_html__( 'Match Any Fields', 'gravityview' ),
72 33
					'all' => esc_html__( 'Match All Fields', 'gravityview' ),
73
				),
74
			),
75
		);
76
77 33
		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 33
		parent::__construct( esc_html__( 'Search Bar', 'gravityview' ), null, $default_values, $settings );
99
100
		// calculate the search method (POST / GET)
101 33
		$this->set_search_method();
102 33
	}
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 33
	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 33
		$method = apply_filters( 'gravityview/search/method', $this->search_method );
126
127 33
		$method = strtolower( $method );
128
129 33
		$this->search_method = in_array( $method, array( 'get', 'post' ) ) ? $method : 'get';
130 33
	}
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
		);
166
167
		/**
168
		 * @filter `gravityview/search/input_types` Change the types of search fields available to a field type
169
		 * @see GravityView_Widget_Search::get_search_input_labels() for the available input types
170
		 * @param array $input_types Associative array: key is field `name`, value is array of GravityView input types (note: use `input_text` for `text`)
171
		 */
172
		$input_types = apply_filters( 'gravityview/search/input_types', $input_types );
173
174
		return $input_types;
175
	}
176
177
	/**
178
	 * Get labels for different types of search bar inputs
179
	 *
180
	 * @since 1.17.5
181
	 *
182
	 * @return array [input type] => input type label
183
	 */
184
	public static function get_search_input_labels() {
185
		/**
186
		 * Input Type labels l10n
187
		 * @see admin-search-widget.js (getSelectInput)
188
		 * @var array
189
		 */
190
		$input_labels = array(
191
			'input_text' => esc_html__( 'Text', 'gravityview' ),
192
			'date' => esc_html__( 'Date', 'gravityview' ),
193
			'select' => esc_html__( 'Select', 'gravityview' ),
194
			'multiselect' => esc_html__( 'Select (multiple values)', 'gravityview' ),
195
			'radio' => esc_html__( 'Radio', 'gravityview' ),
196
			'checkbox' => esc_html__( 'Checkbox', 'gravityview' ),
197
			'single_checkbox' => esc_html__( 'Checkbox', 'gravityview' ),
198
			'link' => esc_html__( 'Links', 'gravityview' ),
199
			'date_range' => esc_html__( 'Date range', 'gravityview' ),
200
		);
201
202
		/**
203
		 * @filter `gravityview/search/input_types` Change the label of search field input types
204
		 * @param array $input_types Associative array: key is input type name, value is label
205
		 */
206
		$input_labels = apply_filters( 'gravityview/search/input_labels', $input_labels );
207
208
		return $input_labels;
209
	}
210
211
	public static function get_search_input_label( $input_type ) {
212
		$labels = self::get_search_input_labels();
213
214
		return \GV\Utils::get( $labels, $input_type, false );
215
	}
216
217
	/**
218
	 * Add script to Views edit screen (admin)
219
	 * @param  mixed $hook
220
	 */
221
	public function add_scripts_and_styles( $hook ) {
222
		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...
223
224
		// Don't process any scripts below here if it's not a GravityView page or the widgets screen
225
		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...
226
			return;
227
		}
228
229
		$script_min = ( defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ) ? '' : '.min';
230
		$script_source = empty( $script_min ) ? '/source' : '';
231
232
		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 );
233
234
		wp_localize_script( 'gravityview_searchwidget_admin', 'gvSearchVar', array(
235
			'nonce' => wp_create_nonce( 'gravityview_ajaxsearchwidget' ),
236
			'label_nofields' => esc_html__( 'No search fields configured yet.', 'gravityview' ),
237
			'label_addfield' => esc_html__( 'Add Search Field', 'gravityview' ),
238
			'label_label' => esc_html__( 'Label', 'gravityview' ),
239
			'label_searchfield' => esc_html__( 'Search Field', 'gravityview' ),
240
			'label_inputtype' => esc_html__( 'Input Type', 'gravityview' ),
241
			'label_ajaxerror' => esc_html__( 'There was an error loading searchable fields. Save the View or refresh the page to fix this issue.', 'gravityview' ),
242
			'input_labels' => json_encode( self::get_search_input_labels() ),
243
			'input_types' => json_encode( self::get_input_types_by_field_type() ),
244
		) );
245
246
	}
247
248
	/**
249
	 * Add admin script to the no-conflict scripts whitelist
250
	 * @param array $allowed Scripts allowed in no-conflict mode
251
	 * @return array Scripts allowed in no-conflict mode, plus the search widget script
252
	 */
253
	public function register_no_conflict( $allowed ) {
254
		$allowed[] = 'gravityview_searchwidget_admin';
255
		return $allowed;
256
	}
257
258
	/**
259
	 * Ajax
260
	 * Returns the form fields ( only the searchable ones )
261
	 *
262
	 * @access public
263
	 * @return void
264
	 */
265
	public static function get_searchable_fields() {
266
267
		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...
268
			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...
269
		}
270
271
		$form = '';
272
273
		// Fetch the form for the current View
274
		if ( ! empty( $_POST['view_id'] ) ) {
275
276
			$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...
277
278
		} elseif ( ! empty( $_POST['formid'] ) ) {
279
280
			$form = (int) $_POST['formid'];
0 ignored issues
show
introduced by
Detected access of super global var $_POST, probably need manual inspection.
Loading history...
281
282
		} elseif ( ! empty( $_POST['template_id'] ) && class_exists( 'GravityView_Ajax' ) ) {
283
284
			$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...
285
286
		}
287
288
		// fetch form id assigned to the view
289
		$response = self::render_searchable_fields( $form );
290
291
		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...
292
	}
293
294
	/**
295
	 * Generates html for the available Search Fields dropdown
296
	 * @param  int $form_id
297
	 * @param  string $current (for future use)
298
	 * @return string
299
	 */
300
	public static function render_searchable_fields( $form_id = null, $current = '' ) {
301
302
		if ( is_null( $form_id ) ) {
303
			return '';
304
		}
305
306
		// start building output
307
308
		$output = '<select class="gv-search-fields">';
309
310
		$custom_fields = array(
311
			'search_all' => array(
312
				'text' => esc_html__( 'Search Everything', 'gravityview' ),
313
				'type' => 'text',
314
			),
315
			'entry_date' => array(
316
				'text' => esc_html__( 'Entry Date', 'gravityview' ),
317
				'type' => 'date',
318
			),
319
			'entry_id' => array(
320
				'text' => esc_html__( 'Entry ID', 'gravityview' ),
321
				'type' => 'text',
322
			),
323
			'created_by' => array(
324
				'text' => esc_html__( 'Entry Creator', 'gravityview' ),
325
				'type' => 'created_by',
326
			),
327
			'is_starred' => array(
328
				'text' => esc_html__( 'Is Starred', 'gravityview' ),
329
				'type' => 'boolean',
330
			),
331
		);
332
333
		if ( gravityview()->plugin->supports( \GV\Plugin::FEATURE_GFQUERY ) ) {
334
			$custom_fields['is_approved'] = array(
335
				'text' => esc_html__( 'Approval Status', 'gravityview' ),
336
				'type' => 'multi',
337
			);
338
		}
339
340
		foreach( $custom_fields as $custom_field_key => $custom_field ) {
341
			$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...
342
		}
343
344
		// Get fields with sub-inputs and no parent
345
		$fields = gravityview_get_form_fields( $form_id, true, true );
346
347
		/**
348
		 * @filter `gravityview/search/searchable_fields` Modify the fields that are displayed as searchable in the Search Bar dropdown\n
349
		 * @since 1.17
350
		 * @see gravityview_get_form_fields() Used to fetch the fields
351
		 * @see GravityView_Widget_Search::get_search_input_types See this method to modify the type of input types allowed for a field
352
		 * @param array $fields Array of searchable fields, as fetched by gravityview_get_form_fields()
353
		 * @param  int $form_id
354
		 */
355
		$fields = apply_filters( 'gravityview/search/searchable_fields', $fields, $form_id );
356
357
		if ( ! empty( $fields ) ) {
358
359
			$blacklist_field_types = apply_filters( 'gravityview_blacklist_field_types', array( 'fileupload', 'post_image', 'post_id', 'section' ), null );
360
361
			foreach ( $fields as $id => $field ) {
362
363
				if ( in_array( $field['type'], $blacklist_field_types ) ) {
364
					continue;
365
				}
366
367
				$types = self::get_search_input_types( $id, $field['type'] );
368
369
				$output .= '<option value="'. $id .'" '. selected( $id, $current, false ).'data-inputtypes="'. esc_attr( $types ) .'">'. esc_html( $field['label'] ) .'</option>';
370
			}
371
		}
372
373
		$output .= '</select>';
374
375
		return $output;
376
377
	}
378
379
	/**
380
	 * Assign an input type according to the form field type
381
	 *
382
	 * @see admin-search-widget.js
383
	 *
384
	 * @param string|int|float $field_id Gravity Forms field ID
385
	 * @param string $field_type Gravity Forms field type (also the `name` parameter of GravityView_Field classes)
386
	 *
387
	 * @return string GV field search input type ('multi', 'boolean', 'select', 'date', 'text')
388
	 */
389
	public static function get_search_input_types( $field_id = '', $field_type = null ) {
390
391
		// @todo - This needs to be improved - many fields have . including products and addresses
392
		if ( false !== strpos( (string) $field_id, '.' ) && in_array( $field_type, array( 'checkbox' ) ) || in_array( $field_id, array( 'is_fulfilled' ) ) ) {
393
			$input_type = 'boolean'; // on/off checkbox
394
		} elseif ( in_array( $field_type, array( 'checkbox', 'post_category', 'multiselect' ) ) ) {
395
			$input_type = 'multi'; //multiselect
396
		} elseif ( in_array( $field_type, array( 'select', 'radio' ) ) ) {
397
			$input_type = 'select';
398
		} elseif ( in_array( $field_type, array( 'date' ) ) || in_array( $field_id, array( 'payment_date' ) ) ) {
399
			$input_type = 'date';
400
		} elseif ( in_array( $field_type, array( 'number' ) ) || in_array( $field_id, array( 'payment_amount' ) ) ) {
401
			$input_type = 'number';
402
		} else {
403
			$input_type = 'text';
404
		}
405
406
		/**
407
		 * @filter `gravityview/extension/search/input_type` Modify the search form input type based on field type
408
		 * @since 1.2
409
		 * @since 1.19.2 Added $field_id parameter
410
		 * @param string $input_type Assign an input type according to the form field type. Defaults: `boolean`, `multi`, `select`, `date`, `text`
411
		 * @param string $field_type Gravity Forms field type (also the `name` parameter of GravityView_Field classes)
412
		 * @param string|int|float $field_id ID of the field being processed
413
		 */
414
		$input_type = apply_filters( 'gravityview/extension/search/input_type', $input_type, $field_type, $field_id );
415
416
		return $input_type;
417
	}
418
419
	/**
420
	 * Display hidden fields to add support for sites using Default permalink structure
421
	 *
422
	 * @since 1.8
423
	 * @return array Search fields, modified if not using permalinks
424
	 */
425 4
	public function add_no_permalink_fields( $search_fields, $object, $widget_args = array() ) {
426
		/** @global WP_Rewrite $wp_rewrite */
427 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...
428
429
		// Support default permalink structure
430 4
		if ( false === $wp_rewrite->using_permalinks() ) {
431
432
			// By default, use current post.
433 4
			$post_id = 0;
434
435
			// We're in the WordPress Widget context, and an overriding post ID has been set.
436 4
			if ( ! empty( $widget_args['post_id'] ) ) {
437
				$post_id = absint( $widget_args['post_id'] );
438
			}
439
			// We're in the WordPress Widget context, and the base View ID should be used
440 4
			else if ( ! empty( $widget_args['view_id'] ) ) {
441
				$post_id = absint( $widget_args['view_id'] );
442
			}
443
444 4
			$args = gravityview_get_permalink_query_args( $post_id );
445
446
			// Add hidden fields to the search form
447 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...
448 4
				$search_fields[] = array(
449 4
					'name'  => $key,
450 4
					'input' => 'hidden',
451 4
					'value' => $value,
452
				);
453
			}
454
		}
455
456 4
		return $search_fields;
457
	}
458
459
	/**
460
	 * Get the fields that are searchable for a View
461
	 *
462
	 * @since 2.0
463
	 * @since 2.0.9 Added $with_full_field parameter
464
	 *
465
	 * @param \GV\View|null $view
466
	 * @param bool $with_full_field Return full field array, or just field ID? Default: false (just field ID)
467
	 *
468
	 * TODO: Move to \GV\View, perhaps? And return a Field_Collection
469
	 * TODO: Use in gravityview()->request->is_search() to calculate whether a valid search
470
	 *
471
	 * @return array If no View, returns empty array. Otherwise, returns array of fields configured in widgets and Search Bar for a View
472
	 */
473 29
	private function get_view_searchable_fields( $view, $with_full_field = false ) {
474
475
		/**
476
		 * Find all search widgets on the view and get the searchable fields settings.
477
		 */
478 29
		$searchable_fields = array();
479
480 29
		if ( ! $view ) {
481
			return $searchable_fields;
482
		}
483
484
		/**
485
		 * Include the sidebar Widgets.
486
		 */
487 29
		$widgets = (array) get_option( 'widget_gravityview_search', array() );
488
489 29
		foreach ( $widgets as $widget ) {
490 29
			if ( ! empty( $widget['view_id'] ) && $widget['view_id'] == $view->ID ) {
491
				if( $_fields = json_decode( $widget['search_fields'], true ) ) {
492
					foreach ( $_fields as $field ) {
493
						if ( empty( $field['form_id'] ) ) {
494
							$field['form_id'] = $view->form ? $view->form->ID : 0;
495
						}
496 29
						$searchable_fields[] = $with_full_field ? $field : $field['field'];
497
					}
498
				}
499
			}
500
		}
501
502 29
		foreach ( $view->widgets->by_id( $this->get_widget_id() )->all() as $widget ) {
503 27
			if( $_fields = json_decode( $widget->configuration->get( 'search_fields' ), true ) ) {
504 27
				foreach ( $_fields as $field ) {
505 27
					if ( empty( $field['form_id'] ) ) {
506 27
						$field['form_id'] = $view->form ? $view->form->ID : 0;
507
					}
508 27
					$searchable_fields[] = $with_full_field ? $field : $field['field'];
509
				}
510
			}
511
		}
512
513 29
		return $searchable_fields;
514
	}
515
516
	/** --- Frontend --- */
517
518
	/**
519
	 * Calculate the search criteria to filter entries
520
	 * @param array $search_criteria The search criteria
521
	 * @param int $form_id The form ID
522
	 * @param array $args Some args
523
	 *
524
	 * @param bool $force_search_criteria Whether to suppress GF_Query filter, internally used in self::gf_query_filter
525
	 *
526
	 * @return array
527
	 */
528 61
	public function filter_entries( $search_criteria, $form_id = null, $args = array(), $force_search_criteria = false ) {
529 61
		if ( ! $force_search_criteria && gravityview()->plugin->supports( \GV\Plugin::FEATURE_GFQUERY ) ) {
530
			/**
531
			 * If GF_Query is available, we can construct custom conditions with nested
532
			 * booleans on the query, giving up the old ways of flat search_criteria field_filters.
533
			 */
534 42
			add_action( 'gravityview/view/query', array( $this, 'gf_query_filter' ), 10, 3 );
535 42
			return $search_criteria; // Return the original criteria, GF_Query modification kicks in later
536
		}
537
538 60
		if( 'post' === $this->search_method ) {
539
			$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...
540
		} else {
541 60
			$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...
542
		}
543
544 60
		$view = \GV\View::by_id( \GV\Utils::get( $args, 'id' ) );
545
546 60
		gravityview()->log->debug( 'Requested $_{method}: ', array( 'method' => $this->search_method, 'data' => $get ) );
547
548 60
		if ( empty( $get ) || ! is_array( $get ) ) {
549 38
			return $search_criteria;
550
		}
551
552 30
		$get = stripslashes_deep( $get );
553
554 30
		$get = gv_map_deep( $get, 'rawurldecode' );
555
556
		// Make sure array key is set up
557 30
		$search_criteria['field_filters'] = \GV\Utils::get( $search_criteria, 'field_filters', array() );
558
559 30
		$searchable_fields = $this->get_view_searchable_fields( $view );
560 30
		$searchable_field_objects = $this->get_view_searchable_fields( $view, true );
561
562
		// add free search
563 30
		if ( isset( $get['gv_search'] ) && '' !== $get['gv_search'] && in_array( 'search_all', $searchable_fields ) ) {
564
565 1
			$search_all_value = trim( $get['gv_search'] );
566
567
			/**
568
			 * @filter `gravityview/search-all-split-words` Search for each word separately or the whole phrase?
569
			 * @since 1.20.2
570
			 * @param bool $split_words True: split a phrase into words; False: search whole word only [Default: true]
571
			 */
572 1
			$split_words = apply_filters( 'gravityview/search-all-split-words', true );
573
574 1
			if ( $split_words ) {
575
576
				// Search for a piece
577 1
				$words = explode( ' ', $search_all_value );
578
579 1
				$words = array_filter( $words );
580
581
			} else {
582
583
				// Replace multiple spaces with one space
584 1
				$search_all_value = preg_replace( '/\s+/ism', ' ', $search_all_value );
585
586 1
				$words = array( $search_all_value );
587
			}
588
589 1
			foreach ( $words as $word ) {
590 1
				$search_criteria['field_filters'][] = array(
591 1
					'key' => null, // The field ID to search
592 1
					'value' => $word, // The value to search
593 1
					'operator' => 'contains', // What to search in. Options: `is` or `contains`
594
				);
595
			}
596
		}
597
598
		// start date & end date
599 30
		if ( in_array( 'entry_date', $searchable_fields ) ) {
600
			/**
601
			 * Get and normalize the dates according to the input format.
602
			 */
603 12
			if ( $curr_start = ! empty( $get['gv_start'] ) ? $get['gv_start'] : '' ) {
604 12
				if( $curr_start_date = date_create_from_format( $this->get_datepicker_format( true ), $curr_start ) ) {
605 12
					$curr_start = $curr_start_date->format( 'Y-m-d' );
606
				}
607
			}
608
609 12
			if ( $curr_end = ! empty( $get['gv_start'] ) ? ( ! empty( $get['gv_end'] ) ? $get['gv_end'] : '' ) : '' ) {
610 12
				if( $curr_end_date = date_create_from_format( $this->get_datepicker_format( true ), $curr_end ) ) {
611 12
					$curr_end = $curr_end_date->format( 'Y-m-d' );
612
				}
613
			}
614
615 12
			if ( $view ) {
616
				/**
617
				 * Override start and end dates if View is limited to some already.
618
				 */
619 12
				if ( $start_date = $view->settings->get( 'start_date' ) ) {
620 1
					if ( $start_timestamp = strtotime( $curr_start ) ) {
621 1
						$curr_start = $start_timestamp < strtotime( $start_date ) ? $start_date : $curr_start;
622
					}
623
				}
624 12
				if ( $end_date = $view->settings->get( 'end_date' ) ) {
625
					if ( $end_timestamp = strtotime( $curr_end ) ) {
626
						$curr_end = $end_timestamp > strtotime( $end_date ) ? $end_date : $curr_end;
627
					}
628
				}
629
			}
630
631
			/**
632
			 * @filter `gravityview_date_created_adjust_timezone` Whether to adjust the timezone for entries. \n
633
			 * date_created is stored in UTC format. Convert search date into UTC (also used on templates/fields/date_created.php)
634
			 * @since 1.12
635
			 * @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
636
			 * @param[in] string $context Where the filter is being called from. `search` in this case.
637
			 */
638 12
			$adjust_tz = apply_filters( 'gravityview_date_created_adjust_timezone', true, 'search' );
639
640
			/**
641
			 * Don't set $search_criteria['start_date'] if start_date is empty as it may lead to bad query results (GFAPI::get_entries)
642
			 */
643 12
			if ( ! empty( $curr_start ) ) {
644 12
				$curr_start = date( 'Y-m-d H:i:s', strtotime( $curr_start ) );
645 12
				$search_criteria['start_date'] = $adjust_tz ? get_gmt_from_date( $curr_start ) : $curr_start;
646
			}
647
648 12
			if ( ! empty( $curr_end ) ) {
649
				// Fast-forward 24 hour on the end time
650 12
				$curr_end = date( 'Y-m-d H:i:s', strtotime( $curr_end ) + DAY_IN_SECONDS );
651 12
				$search_criteria['end_date'] = $adjust_tz ? get_gmt_from_date( $curr_end ) : $curr_end;
652 12
				if ( strpos( $search_criteria['end_date'], '00:00:00' ) ) { // See https://github.com/gravityview/GravityView/issues/1056
653 12
					$search_criteria['end_date'] = date( 'Y-m-d H:i:s', strtotime( $search_criteria['end_date'] ) - 1 );
654
				}
655
			}
656
		}
657
658
		// search for a specific entry ID
659 30
		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...
660 2
			$search_criteria['field_filters'][] = array(
661 2
				'key' => 'id',
662 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...
663 2
				'operator' => $this->get_operator( $get, 'gv_id', array( '=' ), '=' ),
664
			);
665
		}
666
667
		// search for a specific Created_by ID
668 30
		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...
669 4
			$search_criteria['field_filters'][] = array(
670 4
				'key' => 'created_by',
671 4
				'value' => $get['gv_by'],
672 4
				'operator' => $this->get_operator( $get, 'gv_by', array( '=' ), '=' ),
673
			);
674
		}
675
676
		// Get search mode passed in URL
677 30
		$mode = isset( $get['mode'] ) && in_array( $get['mode'], array( 'any', 'all' ) ) ?  $get['mode'] : 'any';
678
679
		// get the other search filters
680 30
		foreach ( $get as $key => $value ) {
681
682 30
			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...
683 18
				continue; // Not a filter, or empty
684
			}
685
686 15
			if ( strpos( $key, '|op' ) !== false ) {
0 ignored issues
show
introduced by
Found "!== false". Use Yoda Condition checks, you must
Loading history...
687
				continue; // This is an operator
688 15
			}
689 2
690
			$filter_key = $this->convert_request_key_to_filter_key( $key );
691
692 14
			if ( ! $filter = $this->prepare_field_filter( $filter_key, $value, $view, $searchable_field_objects, $get ) ) {
693
				continue;
694
			}
695
696
			if ( ! isset( $filter['operator'] ) ) {
697
				$filter['operator'] = $this->get_operator( $get, $key, array( 'contains' ), 'contains' );
698
			}
699 14
700 14
			if ( isset( $filter[0]['value'] ) ) {
701
				$search_criteria['field_filters'] = array_merge( $search_criteria['field_filters'], $filter );
702
703
				// if date range type, set search mode to ALL
704
				if ( ! empty( $filter[0]['operator'] ) && in_array( $filter[0]['operator'], array( '>=', '<=', '>', '<' ) ) ) {
705
					$mode = 'all';
706
				}
707
			} elseif( !empty( $filter ) ) {
0 ignored issues
show
introduced by
Expected 1 space after "!"; 0 found
Loading history...
708
				$search_criteria['field_filters'][] = $filter;
709 30
			}
710
		}
711 30
712
		/**
713 30
		 * @filter `gravityview/search/mode` Set the Search Mode (`all` or `any`)
714
		 * @since 1.5.1
715 30
		 * @param[out,in] string $mode Search mode (`any` vs `all`)
716
		 */
717
		$search_criteria['field_filters']['mode'] = apply_filters( 'gravityview/search/mode', $mode );
718
719
		gravityview()->log->debug( 'Returned Search Criteria: ', array( 'data' => $search_criteria ) );
720
721
		unset( $get );
722
723
		return $search_criteria;
724
	}
725
726
	/**
727 41
	 * Filters the \GF_Query with advanced logic.
728
	 *
729
	 * Dropin for the legacy flat filters when \GF_Query is available.
730
	 *
731
	 * @param \GF_Query $query The current query object reference
732 41
	 * @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...
733
	 * @param \GV\Request $request The request object
734
	 */
735
	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...
736
		/**
737 41
		 * This is a shortcut to get all the needed search criteria.
738 41
		 * We feed these into an new GF_Query and tack them onto the current object.
739 41
		 */
740
		$search_criteria = $this->filter_entries( array(), null, array( 'id' => $view->ID ), true /** force search_criteria */ );
741 41
742
		/**
743 41
		 * Call any userland filters that they might have.
744 37
		 */
745
		remove_filter( 'gravityview_fe_search_criteria', array( $this, 'filter_entries' ), 10, 3 );
746
		$search_criteria = apply_filters( 'gravityview_fe_search_criteria', $search_criteria, $view->form->ID, $view->settings->as_atts() );
747 11
		add_filter( 'gravityview_fe_search_criteria', array( $this, 'filter_entries' ), 10, 3 );
748 11
749 9
		$query_class = $view->get_query_class();
750 9
751
		if ( empty( $search_criteria['field_filters'] ) ) {
752 9
			return;
753
		}
754 9
755 9
		$widgets = $view->widgets->by_id( $this->widget_id );
756 9
		if ( $widgets->count() ) {
757
			$widgets = $widgets->all();
758
			$widget  = $widgets[0];
759
760
			$search_fields = json_decode( $widget->configuration->get( 'search_fields' ), true );
761 11
762 11
			foreach ( (array) $search_fields as $search_field ) {
763
				if ( 'created_by' === $search_field['field'] && 'input_text' === $search_field['input'] ) {
764 11
					$created_by_text_mode = true;
765 11
				}
766 11
			}
767 11
		}
768
769 11
		$extra_conditions = array();
770
		$mode = 'any';
771
772
		foreach ( $search_criteria['field_filters'] as &$filter ) {
773 8
			if ( ! is_array( $filter ) ) {
774 2
				if ( in_array( strtolower( $filter ), array( 'any', 'all' ) ) ) {
775 2
					$mode = $filter;
776
				}
777 2
				continue;
778 2
			}
779 2
780
			// Construct a manual query for unapproved statuses
781
			if ( 'is_approved' === $filter['key'] && in_array( \GravityView_Entry_Approval_Status::UNAPPROVED, (array) $filter['value'] ) ) {
782
				$_tmp_query       = new $query_class( $view->form->ID, array(
783
					'field_filters' => array(
784
						array(
785
							'operator' => 'in',
786 2
							'key'      => 'is_approved',
787
							'value'    => (array) $filter['value'],
788
						),
789 2
						array(
790
							'operator' => 'is',
791 2
							'key'      => 'is_approved',
792
							'value'    => '',
793 2
						),
794 2
						'mode' => 'any'
0 ignored issues
show
introduced by
Key specified for array entry; first entry has no key
Loading history...
795
					),
796
				) );
797
				$_tmp_query_parts = $_tmp_query->_introspect();
798 8
799 1
				$extra_conditions[] = $_tmp_query_parts['where'];
800 1
801 1
				$filter = false;
802
				continue;
803
			}
804
805 7
			// Construct manual query for text mode creator search
806
			if ( 'created_by' === $filter['key'] && ! empty( $created_by_text_mode ) ) {
807
				$extra_conditions[] = new GravityView_Widget_Search_Author_GF_Query_Condition( $filter, $view );
808 7
				$filter = false;
809 3
				continue;
810
			}
811
812
			// By default, we want searches to be wildcard for each field.
813 7
			$filter['operator'] = empty( $filter['operator'] ) ? 'contains' : $filter['operator'];
814 1
815
			// For multichoice, let's have an in (OR) search.
816
			if ( is_array( $filter['value'] ) ) {
817
				$filter['operator'] = 'in'; // @todo what about in contains (OR LIKE chains)?
818
			}
819
820
			// Default form with joins functionality
821
			if ( empty( $filter['form_id'] ) ) {
822
				$filter['form_id'] = $view->form ? $view->form->ID : 0;
823
			}
824 7
825
			/**
826
			 * @filter `gravityview_search_operator` Modify the search operator for the field (contains, is, isnot, etc)
827 11
			 * @param string $operator Existing search operator
828 1
			 * @param array $filter array with `key`, `value`, `operator`, `type` keys
829
			 * @since develop
830 1
			 * @param \GV\View $view The View we're operating on.
831 1
			 */
832
			$filter['operator'] = apply_filters( 'gravityview_search_operator', $filter['operator'], $filter, $view );
833
		}
834 1
835 1
		if ( ! empty( $search_criteria['start_date'] ) || ! empty( $search_criteria['end_date'] ) ) {
836
			$date_criteria = array();
837
838 1
			if ( isset( $search_criteria['start_date'] ) ) {
839 1
				$date_criteria['start_date'] = $search_criteria['start_date'];
840 1
			}
841
842
			if ( isset( $search_criteria['end_date'] ) ) {
843 11
				$date_criteria['end_date'] = $search_criteria['end_date'];
844
			}
845 11
846
			$_tmp_query         = new $query_class( $view->form->ID, $date_criteria );
847 11
			$_tmp_query_parts   = $_tmp_query->_introspect();
848 11
			$extra_conditions[] = $_tmp_query_parts['where'];
849 7
		}
850
851
		$search_conditions = array();
852
853
		if ( $filters = array_filter( $search_criteria['field_filters'] ) ) {
854
855
			foreach ( $filters as $filter ) {
856
				if ( ! is_array( $filter ) ) {
857
					continue;
858 7
				}
859 7
860 7
				/**
861
				 * Parse the filter criteria to generate the needed
862 7
				 * WHERE condition. This is a trick to not write our own generation
863
				 * code by reusing what's inside GF_Query already as they
864
				 * take care of many small things like forcing numeric, etc.
865
				 */
866
				$_tmp_query       = new $query_class( $filter['form_id'], array( 'mode' => 'any', 'field_filters' => array( $filter ) ) );
867 7
				$_tmp_query_parts = $_tmp_query->_introspect();
868 7
				$search_condition = $_tmp_query_parts['where'];
869
870 7
				if ( empty( $filter['key'] ) &&  $search_condition->expressions ) {
871
					 foreach ( $search_condition->expressions as $condition ) {
872
						$search_conditions[] = new GravityView_Widget_Search_All_GF_Query_Condition( $condition, $view );
873 7
					 }
874 7
				} else {
875 7
					$left = $search_condition->left;
876 7
					$alias = $query->_alias( $left->field_id, $left->source, $left->is_entry_column() ? 't' : 'm' );
877
878
					if ( $view->joins && $left->field_id == GF_Query_Column::META ) {
879
						$search_conditions[] = new GravityView_Widget_Search_All_GF_Query_Condition( $search_condition, $view );
880
					} else {
881
						$search_conditions[] = new GF_Query_Condition(
882 11
							new GF_Query_Column( $left->field_id, $left->source, $alias ),
883 7
							$search_condition->operator,
884
							$search_condition->right
885
						);
886
					}
887
				}
888
			}
889
890 11
			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...
891
				$search_conditions = array( call_user_func_array( '\GF_Query_Condition::' . ( $mode == 'all' ? '_and' : '_or' ), $search_conditions ) );
892
			}
893
		}
894
895 11
		/**
896 11
		 * Grab the current clauses. We'll be combining them shortly.
897 11
		 */
898
		$query_parts = $query->_introspect();
899
900
		/**
901
		 * Combine the parts as a new WHERE clause.
902
		 */
903
		$where = call_user_func_array( '\GF_Query_Condition::_and', array_merge( array( $query_parts['where'] ), $search_conditions, $extra_conditions ) );
904
		$query->where( $where );
905
	}
906
907
	/**
908
	 * Convert $_GET/$_POST key to the field/meta ID
909
	 *
910
	 * Examples:
911
	 * - `filter_is_starred` => `is_starred`
912
	 * - `filter_1_2` => `1.2`
913 15
	 * - `filter_5` => `5`
914
	 *
915 15
	 * @since 2.0
916
	 *
917
	 * @param string $key $_GET/_$_POST search key
918 15
	 *
919 13
	 * @return string
920
	 */
921
	private function convert_request_key_to_filter_key( $key ) {
922 15
923
		$field_id = str_replace( 'filter_', '', $key );
924
925
		// calculates field_id, removing 'filter_' and for '_' for advanced fields ( like name or checkbox )
926
		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...
927
			$field_id = str_replace( '_', '.', $field_id );
928
		}
929
930
		return $field_id;
931
	}
932
933
	/**
934
	 * Prepare the field filters to GFAPI
935
	 *
936
	 * 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.
937
	 *
938
	 * Format searched values
939 15
	 *
940 15
	 * @param  string $filter_key ID of the field, or entry meta key
941
	 * @param  string $value $_GET/$_POST search value
942 15
	 * @param  \GV\View $view The view we're looking at
943
	 * @param array[] $searchable_fields The searchable fields as configured by the widget.
944 15
	 * @param string[] $get The $_GET/$_POST array.
945
	 *
946 1
	 * @since develop Added 5th $get parameter for operator overrides.
947
	 * @todo Set function as private.
948 1
	 *
949 1
	 * @return array|false 1 or 2 deph levels, false if not allowed
950
	 */
951
	public function prepare_field_filter( $filter_key, $value, $view, $searchable_fields, $get = array() ) {
952
		$key = $filter_key;
953
		$filter_key = explode( ':', $filter_key ); // field_id, form_id
954
955 1
		$form = null;
956 1
957 1
		if ( count( $filter_key ) > 1 ) {
958 1
			// form is specified
959 1
			list( $field_id, $form_id ) = $filter_key;
960
961
			if ( $forms = \GV\View::get_joined_forms( $view->ID ) ) {
962
				if ( ! $form = \GV\GF_Form::by_id( $form_id ) ) {
963 1
					return false;
964
				}
965
			}
966
967
			// form is allowed
968 1
			$found = false;
969 1
			foreach ( $forms as $form ) {
970 1
				if ( $form->ID == $form_id ) {
971 1
					$found = true;
972 1
					break;
973
				}
974
			}
975
976 1
			if ( ! $found ) {
977 1
				return false;
978
			}
979
980 15
			// form is in searchable fields
981 15
			$found = false;
982 15
			foreach ( $searchable_fields as $field ) {
983 1
				if ( $field_id == $field['field'] && $form->ID == $field['form_id'] ) {
984
					$found = true;
985
					break;
986
				}
987 14
			}
988
989 14
			if ( ! $found ) {
990
				return false;
991
			}
992
		} else {
993 14
			$field_id = reset( $filter_key );
994
			$searchable_fields = wp_list_pluck( $searchable_fields, 'field' );
995
			if ( ! in_array( 'search_all', $searchable_fields ) && ! in_array( $field_id, $searchable_fields ) ) {
996
				return false;
997 14
			}
998 14
		}
999 14
		
1000
		if ( ! $form ) {
1001
			// fallback
1002 14
			$form = $view->form;
1003
		}
1004 14
1005 14
		// get form field array
1006 1
		$form_field = is_numeric( $field_id ) ? \GV\GF_Field::by_id( $form, $field_id ) : \GV\Internal_Field::by_id( $field_id );
1007 1
1008
		// default filter array
1009 13
		$filter = array(
1010
			'key'   => $field_id,
1011
			'value' => $value,
1012
			'form_id' => $form->ID,
1013
		);
1014
1015
		switch ( $form_field->type ) {
1016
1017
			case 'select':
1018
			case 'radio':
1019
				$filter['operator'] = $this->get_operator( $get, $key, array( 'is' ), 'is' );
1020
				break;
1021
1022
			case 'post_category':
1023
1024
				if ( ! is_array( $value ) ) {
1025
					$value = array( $value );
1026
				}
1027
1028
				// Reset filter variable
1029 13
				$filter = array();
1030
1031
				foreach ( $value as $val ) {
1032
					$cat = get_term( $val, 'category' );
1033
					$filter[] = array(
1034
						'key'      => $field_id,
1035
						'value'    => esc_attr( $cat->name ) . ':' . $val,
1036
						'operator' => $this->get_operator( $get, $key, array( 'is' ), 'is' ),
1037
					);
1038
				}
1039
1040
				break;
1041
1042
			case 'multiselect':
1043
1044 13
				if ( ! is_array( $value ) ) {
1045
					break;
1046
				}
1047
1048
				// Reset filter variable
1049
				$filter = array();
1050
1051
				foreach ( $value as $val ) {
1052
					$filter[] = array( 'key' => $field_id, 'value' => $val );
1053
				}
1054
1055
				break;
1056
1057
			case 'checkbox':
1058
				// convert checkbox on/off into the correct search filter
1059
				if ( false !== strpos( $field_id, '.' ) && ! empty( $form_field->inputs ) && ! empty( $form_field->choices ) ) {
1060
					foreach ( $form_field->inputs as $k => $input ) {
1061
						if ( $input['id'] == $field_id ) {
1062
							$filter['value'] = $form_field->choices[ $k ]['value'];
1063
							$filter['operator'] = $this->get_operator( $get, $key, array( 'is' ), 'is' );
1064
							break;
1065
						}
1066
					}
1067
				} elseif ( is_array( $value ) ) {
1068
1069
					// Reset filter variable
1070 13
					$filter = array();
1071 13
1072
					foreach ( $value as $val ) {
1073
						$filter[] = array(
1074
							'key'      => $field_id,
1075
							'value'    => $val,
1076
							'operator' => $this->get_operator( $get, $key, array( 'is' ), 'is' ),
1077
						);
1078
					}
1079
				}
1080
1081
				break;
1082
1083
			case 'name':
1084
			case 'address':
1085
1086
				if ( false === strpos( $field_id, '.' ) ) {
1087
1088
					$words = explode( ' ', $value );
1089
1090
					$filters = array();
1091
					foreach ( $words as $word ) {
1092
						if ( ! empty( $word ) && strlen( $word ) > 1 ) {
1093
							// Keep the same key for each filter
1094
							$filter['value'] = $word;
1095
							// Add a search for the value
1096
							$filters[] = $filter;
1097
						}
1098
					}
1099
1100
					$filter = $filters;
1101
				}
1102
1103
				// State/Province should be exact matches
1104
				if ( 'address' === $form_field->field->type ) {
1105
1106
					$searchable_fields = $this->get_view_searchable_fields( $view, true );
1107
1108
					foreach ( $searchable_fields as $searchable_field ) {
1109
1110
						if( $form_field->ID !== $searchable_field['field'] ) {
1111
							continue;
1112
						}
1113
1114
						// Only exact-match dropdowns, not text search
1115
						if( in_array( $searchable_field['input'], array( 'text', 'search' ), true ) ) {
1116 13
							continue;
1117
						}
1118 8
1119
						$input_id = gravityview_get_input_id_from_id( $form_field->ID );
1120 8
1121
						if ( 4 === $input_id ) {
1122
							$filter['operator'] = $this->get_operator( $get, $key, array( 'is' ), 'is' );
1123
						};
1124
					}
1125
				}
1126
1127
				break;
1128
1129
			case 'date':
1130
1131
				$date_format = $this->get_datepicker_format( true );
1132
1133
				if ( is_array( $value ) ) {
1134
1135
					// Reset filter variable
1136
					$filter = array();
1137
1138
					foreach ( $value as $k => $date ) {
1139
						if ( empty( $date ) ) {
1140
							continue;
1141
						}
1142
						$operator = 'start' === $k ? '>=' : '<=';
1143
1144
						/**
1145
						 * @hack
1146
						 * @since 1.16.3
1147
						 * Safeguard until GF implements '<=' operator
1148 8
						 */
1149 8
						if( !GFFormsModel::is_valid_operator( $operator ) && $operator === '<=' ) {
0 ignored issues
show
introduced by
Expected 1 space after "!"; 0 found
Loading history...
1150
							$operator = '<';
1151
							$date = date( 'Y-m-d', strtotime( self::get_formatted_date( $date, 'Y-m-d', $date_format ) . ' +1 day' ) );
1152 8
						}
1153
1154
						$filter[] = array(
1155
							'key'      => $field_id,
1156
							'value'    => self::get_formatted_date( $date, 'Y-m-d', $date_format ),
1157 14
							'operator' => $this->get_operator( $get, $key, array( $operator ), $operator ),
1158
						);
1159
					}
1160
				} else {
1161
					$date = $value;
1162
					$filter['value'] = self::get_formatted_date( $date, 'Y-m-d', $date_format );
1163
					$filter['operator'] = $this->get_operator( $get, $key, array( 'is' ), 'is' );
1164
				}
1165
1166
				break;
1167
0 ignored issues
show
Coding Style introduced by
Functions must not contain multiple empty lines in a row; found 2 empty lines
Loading history...
1168
1169
		} // switch field type
1170
1171
		return $filter;
1172
	}
1173
1174
	/**
1175
	 * Get the Field Format form GravityForms
1176
	 *
1177
	 * @param GF_Field_Date $field The field object
1178
	 * @since 1.10
1179
	 *
1180
	 * @return string Format of the date in the database
1181
	 */
1182
	public static function get_date_field_format( GF_Field_Date $field ) {
1183
		$format = 'm/d/Y';
1184
		$datepicker = array(
1185
			'mdy' => 'm/d/Y',
1186
			'dmy' => 'd/m/Y',
1187
			'dmy_dash' => 'd-m-Y',
1188
			'dmy_dot' => 'd.m.Y',
1189
			'ymd_slash' => 'Y/m/d',
1190
			'ymd_dash' => 'Y-m-d',
1191
			'ymd_dot' => 'Y.m.d',
1192
		);
1193
1194
		if ( ! empty( $field->dateFormat ) && isset( $datepicker[ $field->dateFormat ] ) ){
1195
			$format = $datepicker[ $field->dateFormat ];
1196
		}
1197
1198 8
		return $format;
1199
	}
1200 8
1201
	/**
1202 8
	 * Format a date value
1203
	 *
1204
	 * @param string $value Date value input
1205
	 * @param string $format Wanted formatted date
1206 8
	 *
1207
	 * @since 2.1.2
1208
	 * @param string $value_format The value format. Default: Y-m-d
1209
	 *
1210
	 * @return string
1211
	 */
1212
	public static function get_formatted_date( $value = '', $format = 'Y-m-d', $value_format = 'Y-m-d' ) {
1213
1214 1
		$date = date_create_from_format( $value_format, $value );
1215
1216
		if ( empty( $date ) ) {
1217 1
			gravityview()->log->debug( 'Date format not valid: {value}', array( 'value' => $value ) );
1218
			return '';
1219 1
		}
1220
		return $date->format( $format );
1221
	}
1222
1223
1224
	/**
1225
	 * Include this extension templates path
1226
	 * @param array $file_paths List of template paths ordered
1227
	 */
1228
	public function add_template_path( $file_paths ) {
1229
1230
		// Index 100 is the default GravityView template path.
1231 4
		$file_paths[102] = self::$file . 'templates/';
1232
1233 4
		return $file_paths;
1234
	}
1235 4
1236 4
	/**
1237
	 * Check whether the configured search fields have a date field
1238 4
	 *
1239
	 * @since 1.17.5
1240
	 *
1241
	 * @param array $search_fields
1242 4
	 *
1243
	 * @return bool True: has a `date` or `date_range` field
1244
	 */
1245
	private function has_date_field( $search_fields ) {
1246
1247
		$has_date = false;
1248
1249
		foreach ( $search_fields as $k => $field ) {
1250
			if ( in_array( $field['input'], array( 'date', 'date_range', 'entry_date' ) ) ) {
1251
				$has_date = true;
1252
				break;
1253 4
			}
1254
		}
1255 4
1256
		return $has_date;
1257 4
	}
1258
1259
	/**
1260
	 * Renders the Search Widget
1261
	 * @param array $widget_args
1262
	 * @param string $content
1263 4
	 * @param string $context
1264
	 *
1265 4
	 * @return void
1266
	 */
1267
	public function render_frontend( $widget_args, $content = '', $context = '' ) {
1268
		/** @var GravityView_View $gravityview_view */
1269
		$gravityview_view = GravityView_View::getInstance();
1270
1271
		if ( empty( $gravityview_view ) ) {
1272 4
			gravityview()->log->debug( '$gravityview_view not instantiated yet.' );
1273
			return;
1274 4
		}
1275
1276 4
		// get configured search fields
1277
		$search_fields = ! empty( $widget_args['search_fields'] ) ? json_decode( $widget_args['search_fields'], true ) : '';
1278 4
1279
		if ( empty( $search_fields ) || ! is_array( $search_fields ) ) {
1280 4
			gravityview()->log->debug( 'No search fields configured for widget:', array( 'data' => $widget_args ) );
1281 4
			return;
1282 4
		}
1283 4
0 ignored issues
show
Coding Style introduced by
Functions must not contain multiple empty lines in a row; found 2 empty lines
Loading history...
1284 4
1285
		// prepare fields
1286
		foreach ( $search_fields as $k => $field ) {
1287
1288
			$updated_field = $field;
1289
1290
			$updated_field = $this->get_search_filter_details( $updated_field );
1291
1292
			switch ( $field['field'] ) {
1293
1294
				case 'search_all':
1295
					$updated_field['key'] = 'search_all';
1296
					$updated_field['input'] = 'search_all';
1297
					$updated_field['value'] = $this->rgget_or_rgpost( 'gv_search' );
1298
					break;
1299
1300
				case 'entry_date':
1301
					$updated_field['key'] = 'entry_date';
1302
					$updated_field['input'] = 'entry_date';
1303
					$updated_field['value'] = array(
1304
						'start' => $this->rgget_or_rgpost( 'gv_start' ),
1305
						'end' => $this->rgget_or_rgpost( 'gv_end' ),
1306
					);
1307
					break;
1308
1309
				case 'entry_id':
1310
					$updated_field['key'] = 'entry_id';
1311
					$updated_field['input'] = 'entry_id';
1312
					$updated_field['value'] = $this->rgget_or_rgpost( 'gv_id' );
1313
					break;
1314
1315 4
				case 'created_by':
1316
					$updated_field['key'] = 'created_by';
1317
					$updated_field['name'] = 'gv_by';
1318 4
					$updated_field['value'] = $this->rgget_or_rgpost( 'gv_by' );
1319
					$updated_field['choices'] = self::get_created_by_choices();
1320
					break;
1321
				
1322
				case 'is_approved':
1323
					$updated_field['key'] = 'is_approved';
1324
					$updated_field['value'] = $this->rgget_or_rgpost( 'filter_is_approved' );
1325
					$updated_field['choices'] = self::get_is_approved_choices();
1326
					break;
1327
			}
1328 4
1329
			$search_fields[ $k ] = $updated_field;
1330 4
		}
1331
1332
		gravityview()->log->debug( 'Calculated Search Fields: ', array( 'data' => $search_fields ) );
1333 4
1334
		/**
1335 4
		 * @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.
1336
		 * @param array $search_fields Array of search filters with `key`, `label`, `value`, `type`, `choices` keys
1337 4
		 * @param GravityView_Widget_Search $this Current widget object
1338
		 * @param array $widget_args Args passed to this method. {@since 1.8}
1339 4
		 * @param \GV\Template_Context $context {@since 2.0}
1340
		 * @var array
1341 4
		 */
1342
		$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...
1343
1344
		$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...
1345
1346 4
		/** @since 1.14 */
1347
		$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...
1348 4
1349 4
		$custom_class = ! empty( $widget_args['custom_class'] ) ? $widget_args['custom_class'] : '';
1350
1351
		$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...
1352
1353
		$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...
1354
1355
		if ( $this->has_date_field( $search_fields ) ) {
1356
			// enqueue datepicker stuff only if needed!
1357
			$this->enqueue_datepicker();
1358 4
		}
1359 4
1360
		$this->maybe_enqueue_flexibility();
1361 4
1362
		$gravityview_view->render( 'widget', 'search', false );
1363 4
	}
1364
1365
	/**
1366
	 * Get the search class for a search form
1367
	 *
1368
	 * @since 1.5.4
1369
	 *
1370
	 * @return string Sanitized CSS class for the search form
1371 4
	 */
1372
	public static function get_search_class( $custom_class = '' ) {
1373
		$gravityview_view = GravityView_View::getInstance();
1374 4
1375
		$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...
1376 4
1377
		if ( ! empty( $custom_class )  ) {
1378
			$search_class .= ' '.$custom_class;
1379
		}
1380
1381
		/**
1382
		 * @filter `gravityview_search_class` Modify the CSS class for the search form
1383
		 * @param string $search_class The CSS class for the search form
1384
		 */
1385
		$search_class = apply_filters( 'gravityview_search_class', $search_class );
1386 4
1387 4
		// Is there an active search being performed? Used by fe-views.js
1388
		$search_class .= GravityView_frontend::getInstance()->isSearch() ? ' gv-is-search' : '';
1389 4
1390
		return gravityview_sanitize_html_class( $search_class );
1391 4
	}
1392
1393 4
1394
	/**
1395
	 * Calculate the search form action
1396
	 * @since 1.6
1397
	 *
1398
	 * @return string
1399
	 */
1400
	public static function get_search_form_action() {
1401
		$gravityview_view = GravityView_View::getInstance();
1402 4
1403
		$post_id = $gravityview_view->getPostId() ? $gravityview_view->getPostId() : $gravityview_view->getViewId();
1404 4
1405
		$url = add_query_arg( array(), get_permalink( $post_id ) );
1406 4
1407
		return esc_url( $url );
1408 4
	}
1409
1410 4
	/**
1411 4
	 * Get the label for a search form field
1412 4
	 * @param  array $field      Field setting as sent by the GV configuration - has `field`, `input` (input type), and `label` keys
1413 4
	 * @param  array $form_field Form field data, as fetched by `gravityview_get_field()`
1414
	 * @return string             Label for the search form
1415
	 */
1416
	private static function get_field_label( $field, $form_field = array() ) {
1417
1418
		$label = \GV\Utils::_GET( 'label', \GV\Utils::get( $field, 'label' ) );
1419
1420
		if ( ! $label ) {
1421
1422
			$label = isset( $form_field['label'] ) ? $form_field['label'] : '';
1423
1424
			switch( $field['field'] ) {
1425
				case 'search_all':
1426
					$label = __( 'Search Entries:', 'gravityview' );
1427
					break;
1428
				case 'entry_date':
1429
					$label = __( 'Filter by date:', 'gravityview' );
1430
					break;
1431
				case 'entry_id':
1432
					$label = __( 'Entry ID:', 'gravityview' );
1433
					break;
1434
				default:
1435
					// If this is a field input, not a field
1436
					if ( strpos( $field['field'], '.' ) > 0 && ! empty( $form_field['inputs'] ) ) {
1437
1438
						// Get the label for the field in question, which returns an array
1439
						$items = wp_list_filter( $form_field['inputs'], array( 'id' => $field['field'] ) );
1440
1441
						// Get the item with the `label` key
1442
						$values = wp_list_pluck( $items, 'label' );
1443
1444
						// There will only one item in the array, but this is easier
1445
						foreach ( $values as $value ) {
1446 4
							$label = $value;
1447
							break;
1448 4
						}
1449
					}
1450
			}
1451
		}
1452
1453
		/**
1454
		 * @filter `gravityview_search_field_label` Modify the label for a search field. Supports returning HTML
1455
		 * @since 1.17.3 Added $field parameter
1456
		 * @param[in,out] string $label Existing label text, sanitized.
1457 4
		 * @param[in] array $form_field Gravity Forms field array, as returned by `GFFormsModel::get_field()`
1458
		 * @param[in] array $field Field setting as sent by the GV configuration - has `field`, `input` (input type), and `label` keys
1459 4
		 */
1460
		$label = apply_filters( 'gravityview_search_field_label', esc_attr( $label ), $form_field, $field );
1461 4
1462
		return $label;
1463
	}
1464 4
1465
	/**
1466
	 * Prepare search fields to frontend render with other details (label, field type, searched values)
1467 4
	 *
1468
	 * @param array $field
1469
	 * @return array
1470 4
	 */
1471
	private function get_search_filter_details( $field ) {
1472
1473 4
		$gravityview_view = GravityView_View::getInstance();
1474 4
1475 4
		$form = $gravityview_view->getForm();
1476 4
1477 4
		// for advanced field ids (eg, first name / last name )
1478 4
		$name = 'filter_' . str_replace( '.', '_', $field['field'] );
1479
1480
		// get searched value from $_GET/$_POST (string or array)
1481
		$value = $this->rgget_or_rgpost( $name );
1482 4
1483
		// get form field details
1484 4
		$form_field = gravityview_get_field( $form, $field['field'] );
1485
1486
		$filter = array(
1487
			'key' => $field['field'],
1488 4
			'name' => $name,
1489
			'label' => self::get_field_label( $field, $form_field ),
1490
			'input' => $field['input'],
1491
			'value' => $value,
1492 4
			'type' => $form_field['type'],
1493
		);
1494
1495
		// collect choices
1496
		if ( 'post_category' === $form_field['type'] && ! empty( $form_field['displayAllCategories'] ) && empty( $form_field['choices'] ) ) {
1497
			$filter['choices'] = gravityview_get_terms_choices();
1498
		} elseif ( ! empty( $form_field['choices'] ) ) {
1499
			$filter['choices'] = $form_field['choices'];
1500
		}
1501
1502
		if ( 'date_range' === $field['input'] && empty( $value ) ) {
1503
			$filter['value'] = array( 'start' => '', 'end' => '' );
1504
		}
1505
1506
		return $filter;
1507
1508
	}
1509
1510
	/**
1511
	 * Calculate the search choices for the users
1512
	 *
1513
	 * @since 1.8
1514
	 *
1515
	 * @return array Array of user choices (value = ID, text = display name)
1516
	 */
1517
	private static function get_created_by_choices() {
1518
1519
		/**
1520
		 * filter gravityview/get_users/search_widget
1521
		 * @see \GVCommon::get_users
1522
		 */
1523
		$users = GVCommon::get_users( 'search_widget', array( 'fields' => array( 'ID', 'display_name' ) ) );
1524
1525
		$choices = array();
1526
		foreach ( $users as $user ) {
1527
			$choices[] = array(
1528
				'value' => $user->ID,
1529
				'text' => $user->display_name,
1530
			);
1531
		}
1532
1533
		return $choices;
1534
	}
1535
1536
	/**
1537
	 * Calculate the search checkbox choices for approval status
1538
	 *
1539
	 * @since develop
1540
	 *
1541
	 * @return array Array of approval status choices (value = status, text = display name)
1542
	 */
1543
	private static function get_is_approved_choices() {
1544
1545
		$choices = array();
1546 4
		foreach ( GravityView_Entry_Approval_Status::get_all() as $status ) {
1547 4
			$choices[] = array(
1548
				'value' => $status['value'],
1549 4
				'text' => $status['label'],
1550
			);
1551
		}
1552
1553
		return $choices;
1554
	}
1555
1556 4
	/**
1557
	 * Output the Clear Search Results button
1558
	 * @since 1.5.4
1559
	 */
1560
	public static function the_clear_search_button() {
1561
		$gravityview_view = GravityView_View::getInstance();
1562
1563
		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...
1564
1565
			$url = strtok( add_query_arg( array() ), '?' );
1566
1567 4
			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...
1568 4
1569
		}
1570 4
	}
1571
1572 4
	/**
1573
	 * Based on the search method, fetch the value for a specific key
1574 4
	 *
1575
	 * @since 1.16.4
1576 4
	 *
1577
	 * @param string $name Name of the request key to fetch the value for
1578
	 *
1579
	 * @return mixed|string Value of request at $name key. Empty string if empty.
1580
	 */
1581
	private function rgget_or_rgpost( $name ) {
1582
		$value = \GV\Utils::_REQUEST( $name );
1583
1584
		$value = stripslashes_deep( $value );
1585
1586
		$value = gv_map_deep( $value, 'rawurldecode' );
1587
1588
		$value = gv_map_deep( $value, '_wp_specialchars' );
1589
1590
		return $value;
1591
	}
1592
1593
1594
	/**
1595
	 * Require the datepicker script for the frontend GV script
1596
	 * @param array $js_dependencies Array of existing required scripts for the fe-views.js script
1597
	 * @return array Array required scripts, with `jquery-ui-datepicker` added
1598
	 */
1599
	public function add_datepicker_js_dependency( $js_dependencies ) {
1600
1601
		$js_dependencies[] = 'jquery-ui-datepicker';
1602
1603
		return $js_dependencies;
1604
	}
1605
1606
	/**
1607
	 * Modify the array passed to wp_localize_script()
1608
	 *
1609
	 * @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...
1610
	 * @param array $view_data View data array with View settings
1611
	 *
1612
	 * @return array
1613
	 */
1614
	public function add_datepicker_localization( $localizations = array(), $view_data = array() ) {
1615
		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...
1616
1617
		/**
1618
		 * @filter `gravityview_datepicker_settings` Modify the datepicker settings
1619
		 * @see http://api.jqueryui.com/datepicker/ Learn what settings are available
1620
		 * @see http://www.renegadetechconsulting.com/tutorials/jquery-datepicker-and-wordpress-i18n Thanks for the helpful information on $wp_locale
1621
		 * @param array $js_localization The data padded to the Javascript file
1622
		 * @param array $view_data View data array with View settings
1623
		 */
1624
		$datepicker_settings = apply_filters( 'gravityview_datepicker_settings', array(
1625
			'yearRange' => '-5:+5',
1626
			'changeMonth' => true,
1627
			'changeYear' => true,
1628
			'closeText' => esc_attr_x( 'Close', 'Close calendar', 'gravityview' ),
1629
			'prevText' => esc_attr_x( 'Prev', 'Previous month in calendar', 'gravityview' ),
1630
			'nextText' => esc_attr_x( 'Next', 'Next month in calendar', 'gravityview' ),
1631
			'currentText' => esc_attr_x( 'Today', 'Today in calendar', 'gravityview' ),
1632
			'weekHeader' => esc_attr_x( 'Week', 'Week in calendar', 'gravityview' ),
1633
			'monthStatus'       => __( 'Show a different month', 'gravityview' ),
1634
			'monthNames'        => array_values( $wp_locale->month ),
1635
			'monthNamesShort'   => array_values( $wp_locale->month_abbrev ),
1636
			'dayNames'          => array_values( $wp_locale->weekday ),
1637
			'dayNamesShort'     => array_values( $wp_locale->weekday_abbrev ),
1638
			'dayNamesMin'       => array_values( $wp_locale->weekday_initial ),
1639
			// get the start of week from WP general setting
1640
			'firstDay'          => get_option( 'start_of_week' ),
1641
			// is Right to left language? default is false
1642
			'isRTL'             => is_rtl(),
1643
		), $view_data );
1644
1645
		$localizations['datepicker'] = $datepicker_settings;
1646
1647
		return $localizations;
1648
1649
	}
1650
1651
	/**
1652
	 * Register search widget scripts, including Flexibility
1653
	 *
1654
	 * @see https://github.com/10up/flexibility
1655
	 *
1656
	 * @since 1.17
1657 4
	 *
1658 4
	 * @return void
1659
	 */
1660
	public function register_scripts() {
1661 4
		wp_register_script( 'gv-flexibility', plugins_url( 'assets/lib/flexibility/flexibility.js', GRAVITYVIEW_FILE ), array(), \GV\Plugin::$version, true );
1662
	}
1663
1664
	/**
1665
	 * If the current visitor is running IE 8 or 9, enqueue Flexibility
1666
	 *
1667
	 * @since 1.17
1668
	 *
1669
	 * @return void
1670
	 */
1671
	private function maybe_enqueue_flexibility() {
1672
		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...
1673
			wp_enqueue_script( 'gv-flexibility' );
1674
		}
1675
	}
1676
1677
	/**
1678
	 * Enqueue the datepicker script
1679
	 *
1680
	 * It sets the $gravityview->datepicker_class parameter
1681
	 *
1682
	 * @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.
1683
	 * @return void
1684
	 */
1685
	public function enqueue_datepicker() {
1686
		$gravityview_view = GravityView_View::getInstance();
1687
1688
		wp_enqueue_script( 'jquery-ui-datepicker' );
1689
1690
		add_filter( 'gravityview_js_dependencies', array( $this, 'add_datepicker_js_dependency' ) );
1691
		add_filter( 'gravityview_js_localization', array( $this, 'add_datepicker_localization' ), 10, 2 );
1692
1693
		$scheme = is_ssl() ? 'https://' : 'http://';
1694
		wp_enqueue_style( 'jquery-ui-datepicker', $scheme.'ajax.googleapis.com/ajax/libs/jqueryui/1.8.18/themes/smoothness/jquery-ui.css' );
1695
1696
		/**
1697
		 * @filter `gravityview_search_datepicker_class`
1698
		 * 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.
1699
		 * @param string $css_class CSS class to use. Default: `gv-datepicker datepicker mdy` \n
1700
		 * Options are:
1701
		 * - `mdy` (mm/dd/yyyy)
1702
		 * - `dmy` (dd/mm/yyyy)
1703
		 * - `dmy_dash` (dd-mm-yyyy)
1704
		 * - `dmy_dot` (dd.mm.yyyy)
1705
		 * - `ymd_slash` (yyyy/mm/dd)
1706
		 * - `ymd_dash` (yyyy-mm-dd)
1707
		 * - `ymd_dot` (yyyy.mm.dd)
1708
		 */
1709 19
		$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...
1710
1711 19
		$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...
1712
	}
1713
1714
	/**
1715
	 * Retrieve the datepicker format.
1716
	 *
1717
	 * @param bool $date_format Whether to return the PHP date format or the datpicker class name. Default: false.
1718
	 *
1719
	 * @see https://docs.gravityview.co/article/115-changing-the-format-of-the-search-widgets-date-picker
1720
	 *
1721
	 * @return string The datepicker format placeholder, or the PHP date format.
1722
	 */
1723
	private function get_datepicker_format( $date_format = false ) {
1724
1725
		$default_format = 'mdy';
1726 19
1727
		/**
1728
		 * @filter `gravityview/widgets/search/datepicker/format`
1729 19
		 * @since 2.1.1
1730
		 * @param string           $format Default: mdy
1731
		 * Options are:
1732
		 * - `mdy` (mm/dd/yyyy)
1733
		 * - `dmy` (dd/mm/yyyy)
1734
		 * - `dmy_dash` (dd-mm-yyyy)
1735
		 * - `dmy_dot` (dd.mm.yyyy)
1736
		 * - `ymd_slash` (yyyy/mm/dd)
1737
		 * - `ymd_dash` (yyyy-mm-dd)
1738
		 * - `ymd_dot` (yyyy.mm.dd)
1739
		 */
1740 19
		$format = apply_filters( 'gravityview/widgets/search/datepicker/format', $default_format );
1741
1742
		$gf_date_formats = array(
1743
			'mdy' => 'm/d/Y',
1744
1745
			'dmy_dash' => 'd-m-Y',
1746 19
			'dmy_dot' => 'd.m.Y',
1747
			'dmy' => 'd/m/Y',
1748
1749
			'ymd_slash' => 'Y/m/d',
1750
			'ymd_dash' => 'Y-m-d',
1751
			'ymd_dot' => 'Y.m.d',
1752
		);
1753
1754
		if ( ! $date_format ) {
1755
			// If the format key isn't valid, return default format key
1756 4
			return isset( $gf_date_formats[ $format ] ) ? $format : $default_format;
1757 4
		}
1758
1759 4
		// If the format key isn't valid, return default format value
1760 4
		return \GV\Utils::get( $gf_date_formats, $format, $gf_date_formats[ $default_format ] );
1761
	}
1762
1763
	/**
1764
	 * If previewing a View or page with embedded Views, make the search work properly by adding hidden fields with query vars
1765
	 *
1766
	 * @since 2.2.1
1767
	 *
1768
	 * @return void
1769
	 */
1770
	public function add_preview_inputs() {
1771
		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...
1772
1773
		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...
1774
			return;
1775
		}
1776
1777
		// 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...
1778
		foreach ( $wp->query_vars as $key => $value ) {
1779
			printf( '<input type="hidden" name="%s" value="%s" />', esc_attr( $key ), esc_attr( $value ) );
1780
		}
1781
1782
	}
1783 1
1784 1
	/**
1785 1
	 * Get an operator URL override.
1786 1
	 *
1787
	 * @param array  $get     Where to look for the operator.
1788 1
	 * @param string $key     The filter key to look for.
1789 1
	 * @param array  $allowed The allowed operators (whitelist).
1790
	 * @param string $default The default operator.
1791
	 *
1792 1
	 * @return string The operator.
1793
	 */
1794
	private function get_operator( $get, $key, $allowed, $default ) {
1795
		$operator = \GV\Utils::get( $get, "$key|op", $default );
1796
1797
		/**
1798
		 * @filter `gravityview/search/operator_whitelist` An array of allowed operators for a field.
1799
		 * @param[in,out] string[] A whitelist of allowed operators.
1800 1
		 * @param string The filter name.
1801
		 */
1802
		$allowed = apply_filters( 'gravityview/search/operator_whitelist', $allowed, $key );
1803 1
1804
		if ( ! in_array( $operator, $allowed, true ) ) {
1805
			$operator = $default;
1806
		}
1807
1808
		return $operator;
1809
	}
1810
1811 1
1812
} // end class
1813 1
1814
new GravityView_Widget_Search;
1815 1
1816 1
if ( ! gravityview()->plugin->supports( \GV\Plugin::FEATURE_GFQUERY ) ) {
1817
	return;
1818
}
1819 1
1820 1
/**
1821
 * A GF_Query condition that allows user data searches.
1822
 */
1823 1
class GravityView_Widget_Search_Author_GF_Query_Condition extends \GF_Query_Condition {
1824
	public function __construct( $filter, $view ) {
1825 1
		$this->value = $filter['value'];
1826
		$this->view = $view;
1827 1
	}
1828
1829
	public function sql( $query ) {
1830
		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...
1831
1832
		$user_meta_fields = array(
1833
			'nickname', 'first_name', 'last_name',
1834
		);
1835
1836
		/**
1837
		 * @filter `gravityview/widgets/search/created_by/user_meta_fields` Filter the user meta fields to search.
1838
		 * @param[in,out] array The user meta fields.
1839
		 * @param \GV\View $view The view.
1840
		 */
1841
		$user_meta_fields = apply_filters( 'gravityview/widgets/search/created_by/user_meta_fields', $user_meta_fields, $this->view );
1842
1843
		$user_fields = array(
1844
			'user_nicename', 'user_login', 'display_name', 'user_email', 
1845
		);
1846
1847
		/**
1848
		 * @filter `gravityview/widgets/search/created_by/user_fields` Filter the user fields to search.
1849
		 * @param[in,out] array The user fields.
1850
		 * @param \GV\View $view The view.
1851
		 */
1852
		$user_fields = apply_filters( 'gravityview/widgets/search/created_by/user_fields', $user_fields, $this->view );
1853
1854
		$conditions = array();
1855
1856
		foreach ( $user_fields as $user_field ) {
1857
			$conditions[] = $wpdb->prepare( "`u`.`$user_field` LIKE %s", '%' . $wpdb->esc_like( $this->value ) .  '%' );
1858
		}
1859
1860
		foreach ( $user_meta_fields as $meta_field ) {
1861
			$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...
1862
		}
1863
1864
		$conditions = '(' . implode( ' OR ', $conditions ) . ')';
1865
1866
		$alias = $query->_alias( null );
1867
1868
		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...
1869
	}
1870
}
1871
1872
/**
1873
 * A GF_Query condition that allows searching across all fields.
1874
 */
1875
class GravityView_Widget_Search_All_GF_Query_Condition extends \GF_Query_Condition {
1876
	public function __construct( $search_condition, $view ) {
1877
		$this->search_condition = $search_condition;
1878
		$this->view = $view;
1879
	}
1880
1881
	public function sql( $query ) {
1882
		// @todo Search limit to only known fields
1883
		$parameters = $query->_introspect();
1884
1885
		// @todo We can search by properties as well in the future
1886
		$table = GFFormsModel::get_entry_meta_table_name();
1887
1888
		$conditions = array();
1889
1890
		foreach ( $parameters['aliases'] as $key => $alias ) {
1891
			if ( 'm' == $alias[0] && preg_match( '#\d+_\d+#', $key ) ) {
1892
				$conditions[] = sprintf( "EXISTS(SELECT * FROM `$table` WHERE `meta_value` %s %s AND `entry_id` = `%s`.`entry_id`)",
1893
					$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...
1894
			}
1895
		}
1896
1897
		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...
1898
			return '(' . implode( ' OR ', $conditions ) . ')';
1899
		}
1900
1901
		return '';
1902
	}
1903
}
1904