Completed
Push — develop ( ce3aa5...163154 )
by Gennady
17:51
created

GravityView_Widget_Search_GF_Query_Condition   A

Complexity

Total Complexity 4

Size/Duplication

Total Lines 48
Duplicated Lines 0 %

Coupling/Cohesion

Components 0
Dependencies 0

Test Coverage

Coverage 0%

Importance

Changes 0
Metric Value
dl 0
loc 48
ccs 0
cts 0
cp 0
rs 10
c 0
b 0
f 0
wmc 4
lcom 0
cbo 0

1 Method

Rating   Name   Duplication   Size   Complexity  
A Condition::__construct() 0 4 1
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 29
	public function __construct() {
31
32 29
		$this->widget_id = 'search_bar';
33 29
		$this->widget_description = esc_html__( 'Search form for searching entries.', 'gravityview' );
34
35 29
		self::$instance = &$this;
36
37 29
		self::$file = plugin_dir_path( __FILE__ );
38
39 29
		$default_values = array( 'header' => 0, 'footer' => 0 );
40
41
		$settings = array(
42 29
			'search_layout' => array(
43 29
				'type' => 'radio',
44
				'full_width' => true,
45 29
				'label' => esc_html__( 'Search Layout', 'gravityview' ),
46 29
				'value' => 'horizontal',
47
				'options' => array(
48 29
					'horizontal' => esc_html__( 'Horizontal', 'gravityview' ),
49 29
					'vertical' => esc_html__( 'Vertical', 'gravityview' ),
50
				),
51
			),
52
			'search_clear' => array(
53 29
				'type' => 'checkbox',
54 29
				'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 29
				'type' => 'radio',
65
				'full_width' => true,
66 29
				'label' => esc_html__( 'Search Mode', 'gravityview' ),
67 29
				'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 29
				'value' => 'any',
69 29
				'class' => 'hide-if-js',
70
				'options' => array(
71 29
					'any' => esc_html__( 'Match Any Fields', 'gravityview' ),
72 29
					'all' => esc_html__( 'Match All Fields', 'gravityview' ),
73
				),
74
			),
75
		);
76
77 29
		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 29
		parent::__construct( esc_html__( 'Search Bar', 'gravityview' ), null, $default_values, $settings );
99
100
		// calculate the search method (POST / GET)
101 29
		$this->set_search_method();
102 29
	}
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 29
	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 29
		$method = apply_filters( 'gravityview/search/method', $this->search_method );
126
127 29
		$method = strtolower( $method );
128
129 29
		$this->search_method = in_array( $method, array( 'get', 'post' ) ) ? $method : 'get';
130 29
	}
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 25
	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 25
		$searchable_fields = array();
479
480 25
		if ( ! $view ) {
481
			return $searchable_fields;
482
		}
483
484
		/**
485
		 * Include the sidebar Widgets.
486
		 */
487 25
		$widgets = (array) get_option( 'widget_gravityview_search', array() );
488
489 25
		foreach ( $widgets as $widget ) {
490 25
			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 25
						if ( empty( $field['form_id'] ) ) {
494
							$field['form_id'] = $view->form ? $view->form->ID : 0;
495
						}
496
						$searchable_fields[] = $with_full_field ? $field : $field['field'];
497
					}
498
				}
499 25
			}
500 24
		}
501 24
502 24
		foreach ( $view->widgets->by_id( $this->get_widget_id() )->all() as $widget ) {
503
			if( $_fields = json_decode( $widget->configuration->get( 'search_fields' ), true ) ) {
504
				foreach ( $_fields as $field ) {
505
					if ( empty( $field['form_id'] ) ) {
506
						$field['form_id'] = $view->form ? $view->form->ID : 0;
507 25
					}
508
					$searchable_fields[] = $with_full_field ? $field : $field['field'];
509
				}
510
			}
511
		}
512
513
		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 56
	 * @param array $args Some args
523 56
	 *
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 37
	public function filter_entries( $search_criteria, $form_id = null, $args = array(), $force_search_criteria = false ) {
529 37
		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 55
			 * booleans on the query, giving up the old ways of flat search_criteria field_filters.
533
			 */
534
			add_action( 'gravityview/view/query', array( $this, 'gf_query_filter' ), 10, 3 );
535 55
			return $search_criteria; // Return the original criteria, GF_Query modification kicks in later
536
		}
537
538 55
		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 55
		} else {
541
			$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 55
		}
543 33
544
		$view = \GV\View::by_id( \GV\Utils::get( $args, 'id' ) );
545
546 26
		gravityview()->log->debug( 'Requested $_{method}: ', array( 'method' => $this->search_method, 'data' => $get ) );
547
548 26
		if ( empty( $get ) || ! is_array( $get ) ) {
549
			return $search_criteria;
550
		}
551 26
552
		$get = stripslashes_deep( $get );
553 26
554
		$get = gv_map_deep( $get, 'rawurldecode' );
555
556 26
		// Make sure array key is set up
557
		$search_criteria['field_filters'] = \GV\Utils::get( $search_criteria, 'field_filters', array() );
558 1
559
		$searchable_fields = $this->get_view_searchable_fields( $view );
560
		$searchable_field_objects = $this->get_view_searchable_fields( $view, true );
561
562
		// add free search
563
		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 1
			/**
568
			 * @filter `gravityview/search-all-split-words` Search for each word separately or the whole phrase?
569
			 * @since 1.20.2
570 1
			 * @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
			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 1
583 1
				// Replace multiple spaces with one space
584 1
				$search_all_value = preg_replace( '/\s+/ism', ' ', $search_all_value );
585 1
586 1
				$words = array( $search_all_value );
587
			}
588
589
			foreach ( $words as $word ) {
590
				$search_criteria['field_filters'][] = array(
591
					'key' => null, // The field ID to search
592 26
					'value' => $word, // The value to search
593
					'operator' => 'contains', // What to search in. Options: `is` or `contains`
594
				);
595
			}
596 11
		}
597 11
598 11
		// start date & end date
599
		if ( in_array( 'entry_date', $searchable_fields ) ) {
600
			/**
601
			 * Get and normalize the dates according to the input format.
602 11
			 */
603 11
			if ( $curr_start = ! empty( $get['gv_start'] ) ? $get['gv_start'] : '' ) {
604 11
				if( $curr_start_date = date_create_from_format( $this->get_datepicker_format( true ), $curr_start ) ) {
605
					$curr_start = $curr_start_date->format( 'Y-m-d' );
606
				}
607
			}
608 11
609
			if ( $curr_end = ! empty( $get['gv_start'] ) ? ( ! empty( $get['gv_end'] ) ? $get['gv_end'] : '' ) : '' ) {
610
				if( $curr_end_date = date_create_from_format( $this->get_datepicker_format( true ), $curr_end ) ) {
611
					$curr_end = $curr_end_date->format( 'Y-m-d' );
612 11
				}
613 1
			}
614 1
615
			if ( $view ) {
616
				/**
617 11
				 * Override start and end dates if View is limited to some already.
618
				 */
619
				if ( $start_date = $view->settings->get( 'start_date' ) ) {
620
					if ( $start_timestamp = strtotime( $curr_start ) ) {
621
						$curr_start = $start_timestamp < strtotime( $start_date ) ? $start_date : $curr_start;
622
					}
623
				}
624
				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 11
			/**
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 11
			 * @param[in] string $context Where the filter is being called from. `search` in this case.
637 11
			 */
638 11
			$adjust_tz = apply_filters( 'gravityview_date_created_adjust_timezone', true, 'search' );
639
640
			/**
641 11
			 * 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 11
			if ( ! empty( $curr_start ) ) {
644 11
				$curr_start = date( 'Y-m-d H:i:s', strtotime( $curr_start ) );
645 11
				$search_criteria['start_date'] = $adjust_tz ? get_gmt_from_date( $curr_start ) : $curr_start;
646 11
			}
647
648
			if ( ! empty( $curr_end ) ) {
649
				// Fast-forward 24 hour on the end time
650
				$curr_end = date( 'Y-m-d H:i:s', strtotime( $curr_end ) + DAY_IN_SECONDS );
651
				$search_criteria['end_date'] = $adjust_tz ? get_gmt_from_date( $curr_end ) : $curr_end;
652 26
				if ( strpos( $search_criteria['end_date'], '00:00:00' ) ) { // See https://github.com/gravityview/GravityView/issues/1056
653 2
					$search_criteria['end_date'] = date( 'Y-m-d H:i:s', strtotime( $search_criteria['end_date'] ) - 1 );
654 2
				}
655 2
			}
656 2
		}
657
658
		// search for a specific entry ID
659
		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
			$search_criteria['field_filters'][] = array(
661 26
				'key' => 'id',
662 4
				'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 4
				'operator' => '=',
664 4
			);
665 4
		}
666
667
		// search for a specific Created_by ID
668
		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
			$search_criteria['field_filters'][] = array(
670 26
				'key' => 'created_by',
671
				'value' => $get['gv_by'],
672
				'operator' => '=',
673 26
			);
674
		}
675 26
676 16
		// Get search mode passed in URL
677
		$mode = isset( $get['mode'] ) && in_array( $get['mode'], array( 'any', 'all' ) ) ?  $get['mode'] : 'any';
678
679 13
		// get the other search filters
680
		foreach ( $get as $key => $value ) {
681
682 13
			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 1
				continue;
684
			}
685
686 12
			$filter_key = $this->convert_request_key_to_filter_key( $key );
687
688 12
			if ( ! $filter = $this->prepare_field_filter( $filter_key, $value, $view, $searchable_field_objects ) ) {
689
				continue;
690
			}
691
692
			if ( isset( $filter[0]['value'] ) ) {
693
				$search_criteria['field_filters'] = array_merge( $search_criteria['field_filters'], $filter );
694
695 12
				// if date range type, set search mode to ALL
696 12
				if ( ! empty( $filter[0]['operator'] ) && in_array( $filter[0]['operator'], array( '>=', '<=', '>', '<' ) ) ) {
697
					$mode = 'all';
698
				}
699
			} elseif( !empty( $filter ) ) {
0 ignored issues
show
introduced by
Expected 1 space after "!"; 0 found
Loading history...
700
				$search_criteria['field_filters'][] = $filter;
701
			}
702
		}
703
704
		/**
705 26
		 * @filter `gravityview/search/mode` Set the Search Mode (`all` or `any`)
706
		 * @since 1.5.1
707 26
		 * @param[out,in] string $mode Search mode (`any` vs `all`)
708
		 */
709 26
		$search_criteria['field_filters']['mode'] = apply_filters( 'gravityview/search/mode', $mode );
710
711 26
		gravityview()->log->debug( 'Returned Search Criteria: ', array( 'data' => $search_criteria ) );
712
713
		unset( $get );
714
715
		return $search_criteria;
716
	}
717
718
	/**
719
	 * Filters the \GF_Query with advanced logic.
720
	 *
721
	 * Dropin for the legacy flat filters when \GF_Query is available.
722
	 *
723 36
	 * @param \GF_Query $query The current query object reference
724
	 * @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...
725
	 * @param \GV\Request $request The request object
726
	 */
727
	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...
728 36
		/**
729
		 * This is a shortcut to get all the needed search criteria.
730 36
		 * We feed these into an new GF_Query and tack them onto the current object.
731 32
		 */
732
		$search_criteria = $this->filter_entries( array(), null, array( 'id' => $view->ID ), true /** force search_criteria */ );
733
734 7
		$query_class = $view->get_query_class();
735 7
736 6
		if ( empty( $search_criteria['field_filters'] ) ) {
737 6
			return;
738
		}
739 6
740
		$widgets = $view->widgets->by_id( $this->widget_id );
741 6
		if ( $widgets->count() ) {
742 6
			$widgets = $widgets->all();
743 6
			$widget  = $widgets[0];
744
745
			$search_fields = json_decode( $widget->configuration->get( 'search_fields' ), true );
746
747
			foreach ( (array) $search_fields as $search_field ) {
748 7
				if ( 'created_by' === $search_field['field'] && 'input_text' === $search_field['input'] ) {
749
					$created_by_text_mode = true;
750 7
				}
751 7
			}
752 7
		}
753
754
		$extra_conditions = array();
755
		$mode = 'any';
756 6
757 2
		foreach ( $search_criteria['field_filters'] as &$filter ) {
758 2
			if ( ! is_array( $filter ) ) {
759
				if ( in_array( strtolower( $filter ), array( 'any', 'all' ) ) ) {
760 2
					$mode = $filter;
761 2
				}
762 2
				continue;
763
			}
764
765
			// Construct a manual query for unapproved statuses
766
			if ( 'is_approved' === $filter['key'] && in_array( \GravityView_Entry_Approval_Status::UNAPPROVED, (array) $filter['value'] ) ) {
767
				$_tmp_query       = new $query_class( $view->form->ID, array(
768
					'field_filters' => array(
769 2
						array(
770
							'operator' => 'in',
771
							'key'      => 'is_approved',
772 2
							'value'    => (array) $filter['value'],
773
						),
774 2
						array(
775
							'operator' => 'is',
776 2
							'key'      => 'is_approved',
777 2
							'value'    => '',
778
						),
779
						'mode' => 'any'
0 ignored issues
show
introduced by
Key specified for array entry; first entry has no key
Loading history...
780
					),
781 6
				) );
782 1
				$_tmp_query_parts = $_tmp_query->_introspect();
783 1
784 1
				$extra_conditions[] = $_tmp_query_parts['where'];
785
786
				$filter = false;
787
				continue;
788 5
			}
789
790
			// Construct manual query for text mode creator search
791 5
			if ( 'created_by' === $filter['key'] && ! empty( $created_by_text_mode ) ) {
792 3
				$extra_conditions[] = new GravityView_Widget_Search_Author_GF_Query_Condition( $filter, $view );
793
				$filter = false;
794
				continue;
795
			}
796
797
			// By default, we want searches to be wildcard for each field.
798
			$filter['operator'] = empty( $filter['operator'] ) ? 'contains' : $filter['operator'];
799
800 5
			// For multichoice, let's have an in (OR) search.
801
			if ( is_array( $filter['value'] ) ) {
802
				$filter['operator'] = 'in'; // @todo what about in contains (OR LIKE chains)?
803 7
			}
804
805
			// Default form with joins functionality
806
			if ( empty( $filter['form_id'] ) ) {
807
				$filter['form_id'] = $view->form ? $view->form->ID : 0;
808
			}
809
810 7
			/**
811 7
			 * @filter `gravityview_search_operator` Modify the search operator for the field (contains, is, isnot, etc)
812
			 * @param string $operator Existing search operator
813
			 * @param array $filter array with `key`, `value`, `operator`, `type` keys
814
			 */
815
			$filter['operator'] = apply_filters( 'gravityview_search_operator', $filter['operator'], $filter );
816 7
		}
817
818
		$search_conditions = array();
819
820
		if ( $filters = array_filter( $search_criteria['field_filters'] ) ) {
821 7
822 7
			foreach ( $filters as $filter ) {
823 7
				if ( ! is_array( $filter ) ) {
824
					continue;
825
				}
826
827
				/**
828
				 * Parse the filter criteria to generate the needed
829
				 * WHERE condition. This is a trick to not write our own generation
830
				 * code by reusing what's inside GF_Query already as they
831
				 * take care of many small things like forcing numeric, etc.
832
				 */
833
				$_tmp_query       = new $query_class( $filter['form_id'], array( 'mode' => 'any', 'field_filters' => array( $filter ) ) );
834
				$_tmp_query_parts = $_tmp_query->_introspect();
835
				$search_condition = $_tmp_query_parts['where'];
836
837
				/**
838
				 * Make sure the column alias is correct.
839 13
				 */
840
				$left = $search_condition->left;
841 13
				$alias = $query->_alias( $left->field_id, $left->source, $left->is_entry_column() ? 't' : 'm' );
842
843
				if ( $view->joins && $left->field_id == GF_Query_Column::META ) {
844 13
					$search_conditions[] = new GravityView_Widget_Search_All_GF_Query_Condition( $search_condition, $view );
845 11
				} else {
846
					$search_conditions[] = new GF_Query_Condition(
847
						new GF_Query_Column( $left->field_id, $left->source, $alias ),
848 13
						$search_condition->operator,
849
						$search_condition->right
850
					);
851
				}
852
			}
853
854
			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...
855
				$search_conditions = array( call_user_func_array( '\GF_Query_Condition::' . ( $mode == 'all' ? '_and' : '_or' ), $search_conditions ) );
856
			}
857
		}
858
859
		/**
860
		 * Grab the current clauses. We'll be combining them shortly.
861
		 */
862
		$query_parts = $query->_introspect();
863
864 12
		/**
865
		 * Combine the parts as a new WHERE clause.
866
		 */
867 12
		$where = call_user_func_array( '\GF_Query_Condition::_and', array_merge( array( $query_parts['where'] ), $search_conditions, $extra_conditions ) );
868
		$query->where( $where );
869
	}
870
871 12
	/**
872 12
	 * Convert $_GET/$_POST key to the field/meta ID
873
	 *
874
	 * Examples:
875 12
	 * - `filter_is_starred` => `is_starred`
876
	 * - `filter_1_2` => `1.2`
877 12
	 * - `filter_5` => `5`
878 12
	 *
879 1
	 * @since 2.0
880 1
	 *
881
	 * @param string $key $_GET/_$_POST search key
882 11
	 *
883
	 * @return string
884
	 */
885
	private function convert_request_key_to_filter_key( $key ) {
886
887
		$field_id = str_replace( 'filter_', '', $key );
888
889
		// calculates field_id, removing 'filter_' and for '_' for advanced fields ( like name or checkbox )
890
		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...
891
			$field_id = str_replace( '_', '.', $field_id );
892
		}
893
894
		return $field_id;
895
	}
896
897
	/**
898
	 * Prepare the field filters to GFAPI
899
	 *
900
	 * 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.
901
	 *
902 11
	 * Format searched values
903
	 *
904
	 * @param  string $filter_key ID of the field, or entry meta key
905
	 * @param  string $value $_GET/$_POST search value
906
	 * @param  \GV\View $view The view we're looking at
907
	 * @param array[] $searchable_fields The searchable fields as configured by the widget.
908
	 *
909
	 * @return array|false 1 or 2 deph levels, false if not allowed
910
	 */
911
	public function prepare_field_filter( $filter_key, $value, $view, $searchable_fields ) {
912
		$filter_key = explode( ':', $filter_key ); // field_id, form_id
913
914
		$form = null;
915
916
		if ( count( $filter_key ) > 1 ) {
917 11
			// form is specified
918
			list( $field_id, $form_id ) = $filter_key;
919
920
			if ( $forms = \GV\View::get_joined_forms( $view->ID ) ) {
921
				if ( ! $form = \GV\GF_Form::by_id( $form_id ) ) {
922
					return false;
923
				}
924
			}
925
926
			// form is allowed
927
			$found = false;
928
			foreach ( $forms as $form ) {
929
				if ( $form->ID == $form_id ) {
930
					$found = true;
931
					break;
932
				}
933
			}
934
935
			if ( ! $found ) {
936
				return false;
937
			}
938
939
			// form is in searchable fields
940
			$found = false;
941
			foreach ( $searchable_fields as $field ) {
942
				if ( $field_id == $field['field'] && $form->ID == $field['form_id'] ) {
943 11
					$found = true;
944 11
					break;
945
				}
946
			}
947
948
			if ( ! $found ) {
949
				return false;
950
			}
951
		} else {
952
			$field_id = reset( $filter_key );
953
			$searchable_fields = wp_list_pluck( $searchable_fields, 'field' );
954
			if ( ! in_array( 'search_all', $searchable_fields ) && ! in_array( $field_id, $searchable_fields ) ) {
955
				return false;
956
			}
957
		}
958
		
959
		if ( ! $form ) {
960
			// fallback
961
			$form = $view->form;
962
		}
963
964
		// get form field array
965
		$form_field = is_numeric( $field_id ) ? \GV\GF_Field::by_id( $form, $field_id ) : \GV\Internal_Field::by_id( $field_id );
966
967
		// default filter array
968
		$filter = array(
969
			'key'   => $field_id,
970
			'value' => $value,
971
			'form_id' => $form->ID,
972
		);
973
974
		switch ( $form_field->type ) {
975
976
			case 'select':
977
			case 'radio':
978
				$filter['operator'] = 'is';
979
				break;
980
981
			case 'post_category':
982
983
				if ( ! is_array( $value ) ) {
984
					$value = array( $value );
985
				}
986
987
				// Reset filter variable
988
				$filter = array();
989 11
990
				foreach ( $value as $val ) {
991 8
					$cat = get_term( $val, 'category' );
992
					$filter[] = array(
993 8
						'key'      => $field_id,
994
						'value'    => esc_attr( $cat->name ) . ':' . $val,
995
						'operator' => 'is',
996
					);
997
				}
998
999
				break;
1000
1001
			case 'multiselect':
1002
1003
				if ( ! is_array( $value ) ) {
1004
					break;
1005
				}
1006
1007
				// Reset filter variable
1008
				$filter = array();
1009
1010
				foreach ( $value as $val ) {
1011
					$filter[] = array( 'key' => $field_id, 'value' => $val );
1012
				}
1013
1014
				break;
1015
1016
			case 'checkbox':
1017
				// convert checkbox on/off into the correct search filter
1018
				if ( false !== strpos( $field_id, '.' ) && ! empty( $form_field->inputs ) && ! empty( $form_field->choices ) ) {
1019
					foreach ( $form_field->inputs as $k => $input ) {
1020
						if ( $input['id'] == $field_id ) {
1021 8
							$filter['value'] = $form_field->choices[ $k ]['value'];
1022 8
							$filter['operator'] = 'is';
1023
							break;
1024
						}
1025 8
					}
1026
				} elseif ( is_array( $value ) ) {
1027
1028
					// Reset filter variable
1029
					$filter = array();
1030 12
1031
					foreach ( $value as $val ) {
1032
						$filter[] = array(
1033
							'key'      => $field_id,
1034
							'value'    => $val,
1035
							'operator' => 'is',
1036
						);
1037
					}
1038
				}
1039
1040
				break;
1041
1042
			case 'name':
1043
			case 'address':
1044
1045
				if ( false === strpos( $field_id, '.' ) ) {
1046
1047
					$words = explode( ' ', $value );
1048
1049
					$filters = array();
1050
					foreach ( $words as $word ) {
1051
						if ( ! empty( $word ) && strlen( $word ) > 1 ) {
1052
							// Keep the same key for each filter
1053
							$filter['value'] = $word;
1054
							// Add a search for the value
1055
							$filters[] = $filter;
1056
						}
1057
					}
1058
1059
					$filter = $filters;
1060
				}
1061
1062
				// State/Province should be exact matches
1063
				if ( 'address' === $form_field->field->type ) {
1064
1065
					$searchable_fields = $this->get_view_searchable_fields( $view, true );
1066
1067
					foreach ( $searchable_fields as $searchable_field ) {
1068
1069
						if( $form_field->ID !== $searchable_field['field'] ) {
1070
							continue;
1071 8
						}
1072
1073 8
						// Only exact-match dropdowns, not text search
1074
						if( in_array( $searchable_field['input'], array( 'text', 'search' ), true ) ) {
1075 8
							continue;
1076
						}
1077
1078
						$input_id = gravityview_get_input_id_from_id( $form_field->ID );
1079 8
1080
						if ( 4 === $input_id ) {
1081
							$filter['operator'] = 'is';
1082
						};
1083
					}
1084
				}
1085
1086
				break;
1087 1
1088
			case 'date':
1089
1090 1
				$date_format = $this->get_datepicker_format( true );
1091
1092 1
				if ( is_array( $value ) ) {
1093
1094
					// Reset filter variable
1095
					$filter = array();
1096
1097
					foreach ( $value as $k => $date ) {
1098
						if ( empty( $date ) ) {
1099
							continue;
1100
						}
1101
						$operator = 'start' === $k ? '>=' : '<=';
1102
1103
						/**
1104 4
						 * @hack
1105
						 * @since 1.16.3
1106 4
						 * Safeguard until GF implements '<=' operator
1107
						 */
1108 4
						if( !GFFormsModel::is_valid_operator( $operator ) && $operator === '<=' ) {
0 ignored issues
show
introduced by
Expected 1 space after "!"; 0 found
Loading history...
1109 4
							$operator = '<';
1110
							$date = date( 'Y-m-d', strtotime( self::get_formatted_date( $date, 'Y-m-d', $date_format ) . ' +1 day' ) );
1111 4
						}
1112
1113
						$filter[] = array(
1114
							'key'      => $field_id,
1115 4
							'value'    => self::get_formatted_date( $date, 'Y-m-d', $date_format ),
1116
							'operator' => $operator,
1117
						);
1118
					}
1119
				} else {
1120
					$date = $value;
1121
					$filter['value'] = self::get_formatted_date( $date, 'Y-m-d', $date_format );
1122
				}
1123
1124
				break;
1125
0 ignored issues
show
Coding Style introduced by
Functions must not contain multiple empty lines in a row; found 2 empty lines
Loading history...
1126 4
1127
		} // switch field type
1128 4
1129
		return $filter;
1130 4
	}
1131
1132
	/**
1133
	 * Get the Field Format form GravityForms
1134
	 *
1135
	 * @param GF_Field_Date $field The field object
1136 4
	 * @since 1.10
1137
	 *
1138 4
	 * @return string Format of the date in the database
1139
	 */
1140
	public static function get_date_field_format( GF_Field_Date $field ) {
1141
		$format = 'm/d/Y';
1142
		$datepicker = array(
1143
			'mdy' => 'm/d/Y',
1144
			'dmy' => 'd/m/Y',
1145 4
			'dmy_dash' => 'd-m-Y',
1146
			'dmy_dot' => 'd.m.Y',
1147 4
			'ymd_slash' => 'Y/m/d',
1148
			'ymd_dash' => 'Y-m-d',
1149 4
			'ymd_dot' => 'Y.m.d',
1150
		);
1151 4
1152
		if ( ! empty( $field->dateFormat ) && isset( $datepicker[ $field->dateFormat ] ) ){
1153 4
			$format = $datepicker[ $field->dateFormat ];
1154 4
		}
1155 4
1156 4
		return $format;
1157 4
	}
1158
1159
	/**
1160
	 * Format a date value
1161
	 *
1162
	 * @param string $value Date value input
1163
	 * @param string $format Wanted formatted date
1164
	 *
1165
	 * @since 2.1.2
1166
	 * @param string $value_format The value format. Default: Y-m-d
1167
	 *
1168
	 * @return string
1169
	 */
1170
	public static function get_formatted_date( $value = '', $format = 'Y-m-d', $value_format = 'Y-m-d' ) {
1171
1172
		$date = date_create_from_format( $value_format, $value );
1173
1174
		if ( empty( $date ) ) {
1175
			gravityview()->log->debug( 'Date format not valid: {value}', array( 'value' => $value ) );
1176
			return '';
1177
		}
1178
		return $date->format( $format );
1179
	}
1180
1181
1182
	/**
1183
	 * Include this extension templates path
1184
	 * @param array $file_paths List of template paths ordered
1185
	 */
1186
	public function add_template_path( $file_paths ) {
1187
1188 4
		// Index 100 is the default GravityView template path.
1189
		$file_paths[102] = self::$file . 'templates/';
1190
1191 4
		return $file_paths;
1192
	}
1193
1194
	/**
1195
	 * Check whether the configured search fields have a date field
1196
	 *
1197
	 * @since 1.17.5
1198
	 *
1199
	 * @param array $search_fields
1200
	 *
1201 4
	 * @return bool True: has a `date` or `date_range` field
1202
	 */
1203 4
	private function has_date_field( $search_fields ) {
1204
1205
		$has_date = false;
1206 4
1207
		foreach ( $search_fields as $k => $field ) {
1208 4
			if ( in_array( $field['input'], array( 'date', 'date_range', 'entry_date' ) ) ) {
1209
				$has_date = true;
1210 4
				break;
1211
			}
1212 4
		}
1213
1214 4
		return $has_date;
1215
	}
1216
1217
	/**
1218
	 * Renders the Search Widget
1219 4
	 * @param array $widget_args
1220
	 * @param string $content
1221 4
	 * @param string $context
1222 4
	 *
1223
	 * @return void
1224
	 */
1225
	public function render_frontend( $widget_args, $content = '', $context = '' ) {
1226
		/** @var GravityView_View $gravityview_view */
1227
		$gravityview_view = GravityView_View::getInstance();
1228
1229
		if ( empty( $gravityview_view ) ) {
1230
			gravityview()->log->debug( '$gravityview_view not instantiated yet.' );
1231 4
			return;
1232 4
		}
1233
1234 4
		// get configured search fields
1235
		$search_fields = ! empty( $widget_args['search_fields'] ) ? json_decode( $widget_args['search_fields'], true ) : '';
1236 4
1237
		if ( empty( $search_fields ) || ! is_array( $search_fields ) ) {
1238
			gravityview()->log->debug( 'No search fields configured for widget:', array( 'data' => $widget_args ) );
1239
			return;
1240
		}
1241
0 ignored issues
show
Coding Style introduced by
Functions must not contain multiple empty lines in a row; found 2 empty lines
Loading history...
1242
1243
		// prepare fields
1244 4
		foreach ( $search_fields as $k => $field ) {
1245
1246
			$updated_field = $field;
1247 4
1248
			$updated_field = $this->get_search_filter_details( $updated_field );
1249 4
1250
			switch ( $field['field'] ) {
1251
1252
				case 'search_all':
1253
					$updated_field['key'] = 'search_all';
1254
					$updated_field['input'] = 'search_all';
1255
					$updated_field['value'] = $this->rgget_or_rgpost( 'gv_search' );
1256
					break;
1257
1258
				case 'entry_date':
1259 4
					$updated_field['key'] = 'entry_date';
1260 4
					$updated_field['input'] = 'entry_date';
1261
					$updated_field['value'] = array(
1262 4
						'start' => $this->rgget_or_rgpost( 'gv_start' ),
1263
						'end' => $this->rgget_or_rgpost( 'gv_end' ),
1264 4
					);
1265
					break;
1266 4
1267
				case 'entry_id':
1268
					$updated_field['key'] = 'entry_id';
1269
					$updated_field['input'] = 'entry_id';
1270
					$updated_field['value'] = $this->rgget_or_rgpost( 'gv_id' );
1271
					break;
1272
1273
				case 'created_by':
1274
					$updated_field['key'] = 'created_by';
1275 4
					$updated_field['name'] = 'gv_by';
1276
					$updated_field['value'] = $this->rgget_or_rgpost( 'gv_by' );
1277 4
					$updated_field['choices'] = self::get_created_by_choices();
1278
					break;
1279 4
				
1280
				case 'is_approved':
1281 4
					$updated_field['key'] = 'is_approved';
1282
					$updated_field['value'] = $this->rgget_or_rgpost( 'filter_is_approved' );
1283 4
					$updated_field['choices'] = self::get_is_approved_choices();
1284 4
					break;
1285 4
			}
1286 4
1287
			$search_fields[ $k ] = $updated_field;
1288
		}
1289
1290
		gravityview()->log->debug( 'Calculated Search Fields: ', array( 'data' => $search_fields ) );
1291
1292
		/**
1293
		 * @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.
1294
		 * @param array $search_fields Array of search filters with `key`, `label`, `value`, `type`, `choices` keys
1295
		 * @param GravityView_Widget_Search $this Current widget object
1296
		 * @param array $widget_args Args passed to this method. {@since 1.8}
1297
		 * @param \GV\Template_Context $context {@since 2.0}
1298
		 * @var array
1299
		 */
1300
		$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...
1301
1302
		$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...
1303
1304
		/** @since 1.14 */
1305
		$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...
1306
1307
		$custom_class = ! empty( $widget_args['custom_class'] ) ? $widget_args['custom_class'] : '';
1308
1309
		$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...
1310
1311
		$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...
1312
1313
		if ( $this->has_date_field( $search_fields ) ) {
1314
			// enqueue datepicker stuff only if needed!
1315
			$this->enqueue_datepicker();
1316
		}
1317
1318
		$this->maybe_enqueue_flexibility();
1319 4
1320
		$gravityview_view->render( 'widget', 'search', false );
1321 4
	}
1322
1323
	/**
1324
	 * Get the search class for a search form
1325
	 *
1326
	 * @since 1.5.4
1327
	 *
1328
	 * @return string Sanitized CSS class for the search form
1329
	 */
1330 4
	public static function get_search_class( $custom_class = '' ) {
1331
		$gravityview_view = GravityView_View::getInstance();
1332 4
1333
		$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...
1334 4
1335
		if ( ! empty( $custom_class )  ) {
1336
			$search_class .= ' '.$custom_class;
1337 4
		}
1338
1339
		/**
1340 4
		 * @filter `gravityview_search_class` Modify the CSS class for the search form
1341
		 * @param string $search_class The CSS class for the search form
1342
		 */
1343 4
		$search_class = apply_filters( 'gravityview_search_class', $search_class );
1344
1345
		// Is there an active search being performed? Used by fe-views.js
1346 4
		$search_class .= GravityView_frontend::getInstance()->isSearch() ? ' gv-is-search' : '';
1347 4
1348 4
		return gravityview_sanitize_html_class( $search_class );
1349 4
	}
1350 4
1351 4
1352
	/**
1353
	 * Calculate the search form action
1354
	 * @since 1.6
1355 4
	 *
1356
	 * @return string
1357 4
	 */
1358
	public static function get_search_form_action() {
1359
		$gravityview_view = GravityView_View::getInstance();
1360
1361 4
		$post_id = $gravityview_view->getPostId() ? $gravityview_view->getPostId() : $gravityview_view->getViewId();
1362
1363
		$url = add_query_arg( array(), get_permalink( $post_id ) );
1364
1365 4
		return esc_url( $url );
1366
	}
1367
1368
	/**
1369
	 * Get the label for a search form field
1370
	 * @param  array $field      Field setting as sent by the GV configuration - has `field`, `input` (input type), and `label` keys
1371
	 * @param  array $form_field Form field data, as fetched by `gravityview_get_field()`
1372
	 * @return string             Label for the search form
1373
	 */
1374
	private static function get_field_label( $field, $form_field = array() ) {
1375
1376
		$label = \GV\Utils::_GET( 'label', \GV\Utils::get( $field, 'label' ) );
1377
1378
		if ( ! $label ) {
1379
1380
			$label = isset( $form_field['label'] ) ? $form_field['label'] : '';
1381
1382
			switch( $field['field'] ) {
1383
				case 'search_all':
1384
					$label = __( 'Search Entries:', 'gravityview' );
1385
					break;
1386
				case 'entry_date':
1387
					$label = __( 'Filter by date:', 'gravityview' );
1388
					break;
1389
				case 'entry_id':
1390
					$label = __( 'Entry ID:', 'gravityview' );
1391
					break;
1392
				default:
1393
					// If this is a field input, not a field
1394
					if ( strpos( $field['field'], '.' ) > 0 && ! empty( $form_field['inputs'] ) ) {
1395
1396
						// Get the label for the field in question, which returns an array
1397
						$items = wp_list_filter( $form_field['inputs'], array( 'id' => $field['field'] ) );
1398
1399
						// Get the item with the `label` key
1400
						$values = wp_list_pluck( $items, 'label' );
1401
1402
						// There will only one item in the array, but this is easier
1403
						foreach ( $values as $value ) {
1404
							$label = $value;
1405
							break;
1406
						}
1407
					}
1408
			}
1409
		}
1410
1411
		/**
1412
		 * @filter `gravityview_search_field_label` Modify the label for a search field. Supports returning HTML
1413
		 * @since 1.17.3 Added $field parameter
1414
		 * @param[in,out] string $label Existing label text, sanitized.
1415
		 * @param[in] array $form_field Gravity Forms field array, as returned by `GFFormsModel::get_field()`
1416
		 * @param[in] array $field Field setting as sent by the GV configuration - has `field`, `input` (input type), and `label` keys
1417
		 */
1418
		$label = apply_filters( 'gravityview_search_field_label', esc_attr( $label ), $form_field, $field );
1419 4
1420 4
		return $label;
1421
	}
1422 4
1423
	/**
1424
	 * Prepare search fields to frontend render with other details (label, field type, searched values)
1425
	 *
1426
	 * @param array $field
1427
	 * @return array
1428
	 */
1429 4
	private function get_search_filter_details( $field ) {
1430
1431
		$gravityview_view = GravityView_View::getInstance();
1432
1433
		$form = $gravityview_view->getForm();
1434
1435
		// for advanced field ids (eg, first name / last name )
1436
		$name = 'filter_' . str_replace( '.', '_', $field['field'] );
1437
1438
		// get searched value from $_GET/$_POST (string or array)
1439
		$value = $this->rgget_or_rgpost( $name );
1440 4
1441 4
		// get form field details
1442
		$form_field = gravityview_get_field( $form, $field['field'] );
1443 4
1444
		$filter = array(
1445 4
			'key' => $field['field'],
1446
			'name' => $name,
1447 4
			'label' => self::get_field_label( $field, $form_field ),
1448
			'input' => $field['input'],
1449 4
			'value' => $value,
1450
			'type' => $form_field['type'],
1451
		);
1452
1453
		// collect choices
1454
		if ( 'post_category' === $form_field['type'] && ! empty( $form_field['displayAllCategories'] ) && empty( $form_field['choices'] ) ) {
1455
			$filter['choices'] = gravityview_get_terms_choices();
1456
		} elseif ( ! empty( $form_field['choices'] ) ) {
1457
			$filter['choices'] = $form_field['choices'];
1458
		}
1459
1460
		if ( 'date_range' === $field['input'] && empty( $value ) ) {
1461
			$filter['value'] = array( 'start' => '', 'end' => '' );
1462
		}
1463
1464
		return $filter;
1465
1466
	}
1467
1468
	/**
1469
	 * Calculate the search choices for the users
1470
	 *
1471
	 * @since 1.8
1472
	 *
1473
	 * @return array Array of user choices (value = ID, text = display name)
1474
	 */
1475
	private static function get_created_by_choices() {
1476
1477
		/**
1478
		 * filter gravityview/get_users/search_widget
1479
		 * @see \GVCommon::get_users
1480
		 */
1481
		$users = GVCommon::get_users( 'search_widget', array( 'fields' => array( 'ID', 'display_name' ) ) );
1482
1483
		$choices = array();
1484
		foreach ( $users as $user ) {
1485
			$choices[] = array(
1486
				'value' => $user->ID,
1487
				'text' => $user->display_name,
1488
			);
1489
		}
1490
1491
		return $choices;
1492
	}
1493
1494
	/**
1495
	 * Calculate the search checkbox choices for approval status
1496
	 *
1497
	 * @since develop
1498
	 *
1499
	 * @return array Array of approval status choices (value = status, text = display name)
1500
	 */
1501
	private static function get_is_approved_choices() {
1502
1503
		$choices = array();
1504
		foreach ( GravityView_Entry_Approval_Status::get_all() as $status ) {
1505
			$choices[] = array(
1506
				'value' => $status['value'],
1507
				'text' => $status['label'],
1508
			);
1509
		}
1510
1511
		return $choices;
1512
	}
1513
1514
	/**
1515
	 * Output the Clear Search Results button
1516
	 * @since 1.5.4
1517
	 */
1518
	public static function the_clear_search_button() {
1519
		$gravityview_view = GravityView_View::getInstance();
1520
1521
		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...
1522
1523
			$url = strtok( add_query_arg( array() ), '?' );
1524
1525
			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...
1526
1527
		}
1528
	}
1529
1530 4
	/**
1531 4
	 * Based on the search method, fetch the value for a specific key
1532
	 *
1533
	 * @since 1.16.4
1534 4
	 *
1535
	 * @param string $name Name of the request key to fetch the value for
1536
	 *
1537
	 * @return mixed|string Value of request at $name key. Empty string if empty.
1538
	 */
1539
	private function rgget_or_rgpost( $name ) {
1540
		$value = \GV\Utils::_REQUEST( $name );
1541
1542
		$value = stripslashes_deep( $value );
1543
1544
		$value = gv_map_deep( $value, 'rawurldecode' );
1545
1546
		$value = gv_map_deep( $value, '_wp_specialchars' );
1547
1548
		return $value;
1549
	}
1550
1551
1552
	/**
1553
	 * Require the datepicker script for the frontend GV script
1554
	 * @param array $js_dependencies Array of existing required scripts for the fe-views.js script
1555
	 * @return array Array required scripts, with `jquery-ui-datepicker` added
1556
	 */
1557
	public function add_datepicker_js_dependency( $js_dependencies ) {
1558
1559
		$js_dependencies[] = 'jquery-ui-datepicker';
1560
1561
		return $js_dependencies;
1562
	}
1563
1564
	/**
1565
	 * Modify the array passed to wp_localize_script()
1566
	 *
1567
	 * @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...
1568
	 * @param array $view_data View data array with View settings
1569
	 *
1570
	 * @return array
1571
	 */
1572
	public function add_datepicker_localization( $localizations = array(), $view_data = array() ) {
1573
		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...
1574
1575
		/**
1576
		 * @filter `gravityview_datepicker_settings` Modify the datepicker settings
1577
		 * @see http://api.jqueryui.com/datepicker/ Learn what settings are available
1578
		 * @see http://www.renegadetechconsulting.com/tutorials/jquery-datepicker-and-wordpress-i18n Thanks for the helpful information on $wp_locale
1579
		 * @param array $js_localization The data padded to the Javascript file
1580
		 * @param array $view_data View data array with View settings
1581
		 */
1582 18
		$datepicker_settings = apply_filters( 'gravityview_datepicker_settings', array(
1583
			'yearRange' => '-5:+5',
1584 18
			'changeMonth' => true,
1585
			'changeYear' => true,
1586
			'closeText' => esc_attr_x( 'Close', 'Close calendar', 'gravityview' ),
1587
			'prevText' => esc_attr_x( 'Prev', 'Previous month in calendar', 'gravityview' ),
1588
			'nextText' => esc_attr_x( 'Next', 'Next month in calendar', 'gravityview' ),
1589
			'currentText' => esc_attr_x( 'Today', 'Today in calendar', 'gravityview' ),
1590
			'weekHeader' => esc_attr_x( 'Week', 'Week in calendar', 'gravityview' ),
1591
			'monthStatus'       => __( 'Show a different month', 'gravityview' ),
1592
			'monthNames'        => array_values( $wp_locale->month ),
1593
			'monthNamesShort'   => array_values( $wp_locale->month_abbrev ),
1594
			'dayNames'          => array_values( $wp_locale->weekday ),
1595
			'dayNamesShort'     => array_values( $wp_locale->weekday_abbrev ),
1596
			'dayNamesMin'       => array_values( $wp_locale->weekday_initial ),
1597
			// get the start of week from WP general setting
1598
			'firstDay'          => get_option( 'start_of_week' ),
1599 18
			// is Right to left language? default is false
1600
			'isRTL'             => is_rtl(),
1601
		), $view_data );
1602 18
1603
		$localizations['datepicker'] = $datepicker_settings;
1604
1605
		return $localizations;
1606
1607
	}
1608
1609
	/**
1610
	 * Register search widget scripts, including Flexibility
1611
	 *
1612
	 * @see https://github.com/10up/flexibility
1613 18
	 *
1614
	 * @since 1.17
1615
	 *
1616
	 * @return void
1617
	 */
1618
	public function register_scripts() {
1619 18
		wp_register_script( 'gv-flexibility', plugins_url( 'assets/lib/flexibility/flexibility.js', GRAVITYVIEW_FILE ), array(), \GV\Plugin::$version, true );
1620
	}
1621
1622
	/**
1623
	 * If the current visitor is running IE 8 or 9, enqueue Flexibility
1624
	 *
1625
	 * @since 1.17
1626
	 *
1627
	 * @return void
1628
	 */
1629 4
	private function maybe_enqueue_flexibility() {
1630 4
		if ( isset( $_SERVER['HTTP_USER_AGENT'] ) && preg_match( '/MSIE [8-9]/', $_SERVER['HTTP_USER_AGENT'] ) ) {
0 ignored issues
show
introduced by
Due to using Batcache, server side based client related logic will not work, use JS instead.
Loading history...
introduced by
Detected usage of a non-sanitized input variable: $_SERVER
Loading history...
1631
			wp_enqueue_script( 'gv-flexibility' );
1632 4
		}
1633 4
	}
1634
1635
	/**
1636
	 * Enqueue the datepicker script
1637
	 *
1638
	 * It sets the $gravityview->datepicker_class parameter
1639
	 *
1640
	 * @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.
1641
	 * @return void
1642
	 */
1643
	public function enqueue_datepicker() {
1644
		$gravityview_view = GravityView_View::getInstance();
1645
1646
		wp_enqueue_script( 'jquery-ui-datepicker' );
1647
1648
		add_filter( 'gravityview_js_dependencies', array( $this, 'add_datepicker_js_dependency' ) );
1649
		add_filter( 'gravityview_js_localization', array( $this, 'add_datepicker_localization' ), 10, 2 );
1650
1651
		$scheme = is_ssl() ? 'https://' : 'http://';
1652
		wp_enqueue_style( 'jquery-ui-datepicker', $scheme.'ajax.googleapis.com/ajax/libs/jqueryui/1.8.18/themes/smoothness/jquery-ui.css' );
1653
1654
		/**
1655
		 * @filter `gravityview_search_datepicker_class`
1656 1
		 * 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.
1657 1
		 * @param string $css_class CSS class to use. Default: `gv-datepicker datepicker mdy` \n
1658 1
		 * Options are:
1659 1
		 * - `mdy` (mm/dd/yyyy)
1660
		 * - `dmy` (dd/mm/yyyy)
1661 1
		 * - `dmy_dash` (dd-mm-yyyy)
1662 1
		 * - `dmy_dot` (dd.mm.yyyy)
1663
		 * - `ymd_slash` (yyyy/mm/dd)
1664
		 * - `ymd_dash` (yyyy-mm-dd)
1665 1
		 * - `ymd_dot` (yyyy.mm.dd)
1666
		 */
1667
		$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...
1668
1669
		$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...
1670
	}
1671
1672
	/**
1673 1
	 * Retrieve the datepicker format.
1674
	 *
1675
	 * @param bool $date_format Whether to return the PHP date format or the datpicker class name. Default: false.
1676 1
	 *
1677
	 * @see https://docs.gravityview.co/article/115-changing-the-format-of-the-search-widgets-date-picker
1678
	 *
1679
	 * @return string The datepicker format placeholder, or the PHP date format.
1680
	 */
1681
	private function get_datepicker_format( $date_format = false ) {
1682
1683
		$default_format = 'mdy';
1684 1
1685
		/**
1686 1
		 * @filter `gravityview/widgets/search/datepicker/format`
1687
		 * @since 2.1.1
1688 1
		 * @param string           $format Default: mdy
1689 1
		 * Options are:
1690
		 * - `mdy` (mm/dd/yyyy)
1691
		 * - `dmy` (dd/mm/yyyy)
1692 1
		 * - `dmy_dash` (dd-mm-yyyy)
1693 1
		 * - `dmy_dot` (dd.mm.yyyy)
1694
		 * - `ymd_slash` (yyyy/mm/dd)
1695
		 * - `ymd_dash` (yyyy-mm-dd)
1696 1
		 * - `ymd_dot` (yyyy.mm.dd)
1697
		 */
1698 1
		$format = apply_filters( 'gravityview/widgets/search/datepicker/format', $default_format );
1699
1700 1
		$gf_date_formats = array(
1701
			'mdy' => 'm/d/Y',
1702
1703
			'dmy_dash' => 'd-m-Y',
1704
			'dmy_dot' => 'd.m.Y',
1705
			'dmy' => 'd/m/Y',
1706
1707
			'ymd_slash' => 'Y/m/d',
1708
			'ymd_dash' => 'Y-m-d',
1709
			'ymd_dot' => 'Y.m.d',
1710
		);
1711
1712
		if ( ! $date_format ) {
1713
			// If the format key isn't valid, return default format key
1714
			return isset( $gf_date_formats[ $format ] ) ? $format : $default_format;
1715
		}
1716
1717
		// If the format key isn't valid, return default format value
1718
		return \GV\Utils::get( $gf_date_formats, $format, $gf_date_formats[ $default_format ] );
1719
	}
1720
1721
	/**
1722
	 * If previewing a View or page with embedded Views, make the search work properly by adding hidden fields with query vars
1723
	 *
1724
	 * @since 2.2.1
1725
	 *
1726
	 * @return void
1727
	 */
1728
	public function add_preview_inputs() {
1729
		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...
1730
1731
		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...
1732
			return;
1733
		}
1734
1735
		// 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...
1736
		foreach ( $wp->query_vars as $key => $value ) {
1737
			printf( '<input type="hidden" name="%s" value="%s" />', esc_attr( $key ), esc_attr( $value ) );
1738
		}
1739
1740
	}
1741
1742
1743
} // end class
1744
1745
new GravityView_Widget_Search;
1746
1747
if ( ! gravityview()->plugin->supports( \GV\Plugin::FEATURE_GFQUERY ) ) {
1748
	return;
1749
}
1750
1751
/**
1752
 * A GF_Query condition that allows user data searches.
1753
 */
1754
class GravityView_Widget_Search_Author_GF_Query_Condition extends \GF_Query_Condition {
1755
	public function __construct( $filter, $view ) {
1756
		$this->value = $filter['value'];
1757
		$this->view = $view;
1758
	}
1759
1760
	public function sql( $query ) {
1761
		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...
1762
1763
		$user_meta_fields = array(
1764
			'nickname', 'first_name', 'last_name',
1765
		);
1766
1767
		/**
1768
		 * @filter `gravityview/widgets/search/created_by/user_meta_fields` Filter the user meta fields to search.
1769
		 * @param[in,out] array The user meta fields.
1770
		 * @param \GV\View $view The view.
1771
		 */
1772
		$user_meta_fields = apply_filters( 'gravityview/widgets/search/created_by/user_meta_fields', $user_meta_fields, $this->view );
1773
1774
		$user_fields = array(
1775
			'user_nicename', 'user_login', 'display_name', 'user_email', 
1776
		);
1777
1778
		/**
1779
		 * @filter `gravityview/widgets/search/created_by/user_fields` Filter the user fields to search.
1780
		 * @param[in,out] array The user fields.
1781
		 * @param \GV\View $view The view.
1782
		 */
1783
		$user_fields = apply_filters( 'gravityview/widgets/search/created_by/user_fields', $user_fields, $this->view );
1784
1785
		$conditions = array();
1786
1787
		foreach ( $user_fields as $user_field ) {
1788
			$conditions[] = $wpdb->prepare( "`u`.`$user_field` LIKE %s", '%' . $wpdb->esc_like( $this->value ) .  '%' );
1789
		}
1790
1791
		foreach ( $user_meta_fields as $meta_field ) {
1792
			$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...
1793
		}
1794
1795
		$conditions = '(' . implode( ' OR ', $conditions ) . ')';
1796
1797
		$alias = $query->_alias( null );
1798
1799
		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...
1800
	}
1801
}
1802
1803
/**
1804
 * A GF_Query condition that allows searching across all fields.
1805
 */
1806
class GravityView_Widget_Search_All_GF_Query_Condition extends \GF_Query_Condition {
1807
	public function __construct( $search_condition, $view ) {
1808
		$this->search_condition = $search_condition;
1809
		$this->view = $view;
1810
	}
1811
1812
	public function sql( $query ) {
1813
		// @todo Search limit to only known fields
1814
		$parameters = $query->_introspect();
1815
1816
		// @todo We can search by properties as well in the future
1817
		$table = GFFormsModel::get_entry_meta_table_name();
1818
1819
		$conditions = array();
1820
1821
		foreach ( $parameters['aliases'] as $key => $alias ) {
1822
			if ( 'm' == $alias[0] && preg_match( '#\d+_\d+#', $key ) ) {
1823
				$conditions[] = sprintf( "EXISTS(SELECT * FROM `$table` WHERE `meta_value` %s %s AND `entry_id` = `%s`.`entry_id`)",
1824
					$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...
1825
			}
1826
		}
1827
1828
		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...
1829
			return '(' . implode( ' OR ', $conditions ) . ')';
1830
		}
1831
1832
		return '';
1833
	}
1834
}
1835