Completed
Push — master ( 2cfa6f...8927a4 )
by Zack
10:00 queued 06:05
created

widgets/search-widget/class-search-widget.php (4 issues)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
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 GravityView_Widget {
17
18
	public static $file;
19
	public static $instance;
20
21
	private $search_filters = array();
22
23
	/**
24
	 * whether search method is GET or POST ( default: GET )
25
	 * @since 1.16.4
26
	 * @var string
27
	 */
28
	private $search_method = 'get';
29
30
	public function __construct() {
31
32
		$this->widget_description = esc_html__( 'Search form for searching entries.', 'gravityview' );
33
34
		self::$instance = &$this;
35
36
		self::$file = plugin_dir_path( __FILE__ );
37
38
		$default_values = array( 'header' => 0, 'footer' => 0 );
39
40
		$settings = array(
41
			'search_layout' => array(
42
				'type' => 'radio',
43
				'full_width' => true,
44
				'label' => esc_html__( 'Search Layout', 'gravityview' ),
45
				'value' => 'horizontal',
46
				'options' => array(
47
					'horizontal' => esc_html__( 'Horizontal', 'gravityview' ),
48
					'vertical' => esc_html__( 'Vertical', 'gravityview' ),
49
				),
50
			),
51
			'search_clear' => array(
52
				'type' => 'checkbox',
53
				'label' => __( 'Show Clear button', 'gravityview' ),
54
				'value' => false,
55
			),
56
			'search_fields' => array(
57
				'type' => 'hidden',
58
				'label' => '',
59
				'class' => 'gv-search-fields-value',
60
				'value' => '[{"field":"search_all","input":"input_text"}]', // Default: Search Everything text box
61
			),
62
			'search_mode' => array(
63
				'type' => 'radio',
64
				'full_width' => true,
65
				'label' => esc_html__( 'Search Mode', 'gravityview' ),
66
				'desc' => __('Should search results match all search fields, or any?', 'gravityview'),
67
				'value' => 'any',
68
				'class' => 'hide-if-js',
69
				'options' => array(
70
					'any' => esc_html__( 'Match Any Fields', 'gravityview' ),
71
					'all' => esc_html__( 'Match All Fields', 'gravityview' ),
72
				),
73
			),
74
		);
75
		parent::__construct( esc_html__( 'Search Bar', 'gravityview' ), 'search_bar', $default_values, $settings );
76
77
		// frontend - filter entries
78
		add_filter( 'gravityview_fe_search_criteria', array( $this, 'filter_entries' ), 10, 1 );
79
80
		// frontend - add template path
81
		add_filter( 'gravityview_template_paths', array( $this, 'add_template_path' ) );
82
83
		// Add hidden fields for "Default" permalink structure
84
		add_filter( 'gravityview_widget_search_filters', array( $this, 'add_no_permalink_fields' ), 10, 3 );
85
86
		// admin - add scripts - run at 1100 to make sure GravityView_Admin_Views::add_scripts_and_styles() runs first at 999
87
		add_action( 'admin_enqueue_scripts', array( $this, 'add_scripts_and_styles' ), 1100 );
88
		add_action( 'wp_enqueue_scripts', array( $this, 'register_scripts') );
89
		add_filter( 'gravityview_noconflict_scripts', array( $this, 'register_no_conflict' ) );
90
91
		// ajax - get the searchable fields
92
		add_action( 'wp_ajax_gv_searchable_fields', array( 'GravityView_Widget_Search', 'get_searchable_fields' ) );
93
94
		// calculate the search method (POST / GET)
95
		$this->set_search_method();
96
97
	}
98
99
	/**
100
	 * @return GravityView_Widget_Search
101
	 */
102
	public static function getInstance() {
103
		if ( empty( self::$instance ) ) {
104
			self::$instance = new GravityView_Widget_Search;
105
		}
106
		return self::$instance;
107
	}
108
109
	/**
110
	 * Sets the search method to GET (default) or POST
111
	 * @since 1.16.4
112
	 */
113
	private function set_search_method() {
114
		/**
115
		 * @filter `gravityview/search/method` Modify the search form method (GET / POST)
116
		 * @since 1.16.4
117
		 * @param string $search_method Assign an input type according to the form field type. Defaults: `boolean`, `multi`, `select`, `date`, `text`
118
		 * @param string $field_type Gravity Forms field type (also the `name` parameter of GravityView_Field classes)
119
		 */
120
		$method = apply_filters( 'gravityview/search/method', $this->search_method );
121
122
		$method = strtolower( $method );
123
124
		$this->search_method = in_array( $method, array( 'get', 'post' ) ) ? $method : 'get';
125
	}
126
127
	/**
128
	 * Returns the search method
129
	 * @since 1.16.4
130
	 * @return string
131
	 */
132
	public function get_search_method() {
133
		return $this->search_method;
134
	}
135
136
	/**
137
	 * Get the input types available for different field types
138
	 *
139
	 * @since 1.17.5
140
	 *
141
	 * @return array [field type name] => (array|string) search bar input types
142
	 */
143
	public static function get_input_types_by_field_type() {
144
		/**
145
		 * Input Type groups
146
		 * @see admin-search-widget.js (getSelectInput)
147
		 * @var array
148
		 */
149
		$input_types = array(
150
			'text' => array( 'input_text' ),
151
			'address' => array( 'input_text' ),
152
			'number' => array( 'input_text' ),
153
			'date' => array( 'date', 'date_range' ),
154
			'boolean' => array( 'single_checkbox' ),
155
			'select' => array( 'select', 'radio', 'link' ),
156
			'multi' => array( 'select', 'multiselect', 'radio', 'checkbox', 'link' ),
157
		);
158
159
		$input_types = apply_filters( 'gravityview/search/input_types', $input_types );
160
161
		return $input_types;
162
	}
163
164
	/**
165
	 * Get labels for different types of search bar inputs
166
	 *
167
	 * @since 1.17.5
168
	 *
169
	 * @return array [input type] => input type label
170
	 */
171
	public static function get_search_input_labels() {
172
		/**
173
		 * Input Type labels l10n
174
		 * @see admin-search-widget.js (getSelectInput)
175
		 * @var array
176
		 */
177
		$input_labels = array(
178
			'input_text' => esc_html__( 'Text', 'gravityview' ),
179
			'date' => esc_html__( 'Date', 'gravityview' ),
180
			'select' => esc_html__( 'Select', 'gravityview' ),
181
			'multiselect' => esc_html__( 'Select (multiple values)', 'gravityview' ),
182
			'radio' => esc_html__( 'Radio', 'gravityview' ),
183
			'checkbox' => esc_html__( 'Checkbox', 'gravityview' ),
184
			'single_checkbox' => esc_html__( 'Checkbox', 'gravityview' ),
185
			'link' => esc_html__( 'Links', 'gravityview' ),
186
			'date_range' => esc_html__( 'Date range', 'gravityview' ),
187
		);
188
189
		$input_labels = apply_filters( 'gravityview/search/input_labels', $input_labels );
190
191
		return $input_labels;
192
	}
193
194
	public static function get_search_input_label( $input_type ) {
195
		$labels = self::get_search_input_labels();
196
197
		return rgar( $labels, $input_type, false );
198
	}
199
200
	/**
201
	 * Add script to Views edit screen (admin)
202
	 * @param  mixed $hook
203
	 */
204
	public function add_scripts_and_styles( $hook ) {
205
		global $pagenow;
206
207
		// Don't process any scripts below here if it's not a GravityView page or the widgets screen
208
		if ( ! gravityview_is_admin_page( $hook ) && ( 'widgets.php' !== $pagenow ) ) {
209
			return;
210
		}
211
212
		$script_min = ( defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ) ? '' : '.min';
213
		$script_source = empty( $script_min ) ? '/source' : '';
214
215
		wp_enqueue_script( 'gravityview_searchwidget_admin', plugins_url( 'assets/js'.$script_source.'/admin-search-widget'.$script_min.'.js', __FILE__ ), array( 'jquery', 'gravityview_views_scripts' ), GravityView_Plugin::version );
216
217
		wp_localize_script( 'gravityview_searchwidget_admin', 'gvSearchVar', array(
218
			'nonce' => wp_create_nonce( 'gravityview_ajaxsearchwidget' ),
219
			'label_nofields' => esc_html__( 'No search fields configured yet.', 'gravityview' ),
220
			'label_addfield' => esc_html__( 'Add Search Field', 'gravityview' ),
221
			'label_label' => esc_html__( 'Label', 'gravityview' ),
222
			'label_searchfield' => esc_html__( 'Search Field', 'gravityview' ),
223
			'label_inputtype' => esc_html__( 'Input Type', 'gravityview' ),
224
			'label_ajaxerror' => esc_html__( 'There was an error loading searchable fields. Save the View or refresh the page to fix this issue.', 'gravityview' ),
225
			'input_labels' => json_encode( self::get_search_input_labels() ),
226
			'input_types' => json_encode( self::get_input_types_by_field_type() ),
227
		) );
228
229
	}
230
231
	/**
232
	 * Add admin script to the no-conflict scripts whitelist
233
	 * @param array $allowed Scripts allowed in no-conflict mode
234
	 * @return array Scripts allowed in no-conflict mode, plus the search widget script
235
	 */
236
	public function register_no_conflict( $allowed ) {
237
		$allowed[] = 'gravityview_searchwidget_admin';
238
		return $allowed;
239
	}
240
241
	/**
242
	 * Ajax
243
	 * Returns the form fields ( only the searchable ones )
244
	 *
245
	 * @access public
246
	 * @return void
247
	 */
248
	public static function get_searchable_fields() {
249
250
		if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( $_POST['nonce'], 'gravityview_ajaxsearchwidget' ) ) {
251
			exit( '0' );
252
		}
253
254
		$form = '';
255
256
		// Fetch the form for the current View
257
		if ( ! empty( $_POST['view_id'] ) ) {
258
259
			$form = gravityview_get_form_id( $_POST['view_id'] );
260
261
		} elseif ( ! empty( $_POST['formid'] ) ) {
262
263
			$form = (int) $_POST['formid'];
264
265
		} elseif ( ! empty( $_POST['template_id'] ) && class_exists( 'GravityView_Ajax' ) ) {
266
267
			$form = GravityView_Ajax::pre_get_form_fields( $_POST['template_id'] );
268
269
		}
270
271
		// fetch form id assigned to the view
272
		$response = self::render_searchable_fields( $form );
273
274
		exit( $response );
275
	}
276
277
	/**
278
	 * Generates html for the available Search Fields dropdown
279
	 * @param  int $form_id
280
	 * @param  string $current (for future use)
281
	 * @return string
282
	 */
283
	public static function render_searchable_fields( $form_id = null, $current = '' ) {
284
285
		if ( is_null( $form_id ) ) {
286
			return '';
287
		}
288
289
		// start building output
290
291
		$output = '<select class="gv-search-fields">';
292
293
		$custom_fields = array(
294
			'search_all' => array(
295
				'text' => esc_html__( 'Search Everything', 'gravityview' ),
296
				'type' => 'text',
297
			),
298
			'entry_date' => array(
299
				'text' => esc_html__( 'Entry Date', 'gravityview' ),
300
				'type' => 'date',
301
			),
302
			'entry_id' => array(
303
				'text' => esc_html__( 'Entry ID', 'gravityview' ),
304
				'type' => 'text',
305
			),
306
			'created_by' => array(
307
				'text' => esc_html__( 'Entry Creator', 'gravityview' ),
308
				'type' => 'select',
309
			)
310
		);
311
312
		foreach( $custom_fields as $custom_field_key => $custom_field ) {
313
			$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'] );
314
		}
315
316
		// Get fields with sub-inputs and no parent
317
		$fields = gravityview_get_form_fields( $form_id, true, true );
318
319
		/**
320
		 * @filter `gravityview/search/searchable_fields` Modify the fields that are displayed as searchable in the Search Bar dropdown\n
321
		 * @since 1.17
322
		 * @see gravityview_get_form_fields() Used to fetch the fields
323
		 * @see GravityView_Widget_Search::get_search_input_types See this method to modify the type of input types allowed for a field
324
		 * @param array $fields Array of searchable fields, as fetched by gravityview_get_form_fields()
325
		 * @param  int $form_id
326
		 */
327
		$fields = apply_filters( 'gravityview/search/searchable_fields', $fields, $form_id );
328
329
		if ( ! empty( $fields ) ) {
330
331
			$blacklist_field_types = apply_filters( 'gravityview_blacklist_field_types', array( 'fileupload', 'post_image', 'post_id', 'section' ), null );
332
333
			foreach ( $fields as $id => $field ) {
334
335
				if ( in_array( $field['type'], $blacklist_field_types ) ) {
336
					continue;
337
				}
338
339
				$types = self::get_search_input_types( $id, $field['type'] );
340
341
				$output .= '<option value="'. $id .'" '. selected( $id, $current, false ).'data-inputtypes="'. esc_attr( $types ) .'">'. esc_html( $field['label'] ) .'</option>';
342
			}
343
		}
344
345
		$output .= '</select>';
346
347
		return $output;
348
349
	}
350
351
	/**
352
	 * Assign an input type according to the form field type
353
	 * @see admin-search-widget.js
354
	 *
355
	 * @param int $id Gravity Forms field ID
356
	 * @param string $field_type Gravity Forms field type (also the `name` parameter of GravityView_Field classes)
357
	 *
358
	 * @return string GV field search input type ('multi', 'boolean', 'select', 'date', 'text')
359
	 */
360
	public static function get_search_input_types( $id = '', $field_type = null ) {
361
362
		// @todo - This needs to be improved - many fields have . including products and addresses
363
		if ( false !== strpos( (string) $id, '.' ) && in_array( $field_type, array( 'checkbox' ) ) || in_array( $id, array( 'is_fulfilled' ) ) ) {
364
			$input_type = 'boolean'; // on/off checkbox
365
		} elseif ( in_array( $field_type, array( 'checkbox', 'post_category', 'multiselect' ) ) ) {
366
			$input_type = 'multi'; //multiselect
367
		} elseif ( in_array( $field_type, array( 'select', 'radio' ) ) ) {
368
			$input_type = 'select';
369
		} elseif ( in_array( $field_type, array( 'date' ) ) || in_array( $id, array( 'payment_date' ) ) ) {
370
			$input_type = 'date';
371
		} elseif ( in_array( $field_type, array( 'number' ) ) || in_array( $id, array( 'payment_amount' ) ) ) {
372
			$input_type = 'number';
373
		} else {
374
			$input_type = 'text';
375
		}
376
377
		/**
378
		 * @filter `gravityview/extension/search/input_type` Modify the search form input type based on field type
379
		 * @since 1.2
380
		 * @param string $input_type Assign an input type according to the form field type. Defaults: `boolean`, `multi`, `select`, `date`, `text`
381
		 * @param string $field_type Gravity Forms field type (also the `name` parameter of GravityView_Field classes)
382
		 */
383
		$input_type = apply_filters( 'gravityview/extension/search/input_type', $input_type, $field_type );
384
385
		return $input_type;
386
	}
387
388
	/**
389
	 * Display hidden fields to add support for sites using Default permalink structure
390
	 *
391
	 * @since 1.8
392
	 * @return array Search fields, modified if not using permalinks
393
	 */
394
	public function add_no_permalink_fields( $search_fields, $object, $widget_args = array() ) {
395
		/** @global WP_Rewrite $wp_rewrite */
396
		global $wp_rewrite;
397
398
		// Support default permalink structure
399
		if ( false === $wp_rewrite->using_permalinks() ) {
400
401
			// By default, use current post.
402
			$post_id = 0;
403
404
			// We're in the WordPress Widget context, and an overriding post ID has been set.
405
			if ( ! empty( $widget_args['post_id'] ) ) {
406
				$post_id = absint( $widget_args['post_id'] );
407
			}
408
			// We're in the WordPress Widget context, and the base View ID should be used
409
			else if ( ! empty( $widget_args['view_id'] ) ) {
410
				$post_id = absint( $widget_args['view_id'] );
411
			}
412
413
			$args = gravityview_get_permalink_query_args( $post_id );
414
415
			// Add hidden fields to the search form
416
			foreach ( $args as $key => $value ) {
417
				$search_fields[] = array(
418
					'name'  => $key,
419
					'input' => 'hidden',
420
					'value' => $value,
421
				);
422
			}
423
		}
424
425
		return $search_fields;
426
	}
427
428
429
	/** --- Frontend --- */
430
431
	/**
432
	 * Calculate the search criteria to filter entries
433
	 * @param  array $search_criteria
434
	 * @return array
435
	 */
436
	public function filter_entries( $search_criteria ) {
437
438
		if( 'post' === $this->search_method ) {
439
			$get = $_POST;
440
		} else {
441
			$get = $_GET;
442
		}
443
		
444
		do_action( 'gravityview_log_debug', sprintf( '%s[filter_entries] Requested $_%s: ', get_class( $this ), $this->search_method ), $get );
445
446
		if ( empty( $get ) || ! is_array( $get ) ) {
447
			return $search_criteria;
448
		}
449
450
		$get = stripslashes_deep( $get );
451
452
		$get = gv_map_deep( $get, 'rawurldecode' );
453
454
		// add free search
455
		if ( ! empty( $get['gv_search'] ) ) {
456
457
			// Search for a piece
458
			$words = explode( ' ', $get['gv_search'] );
459
460
			$words = array_filter( $words );
461
462
			foreach ( $words as $word ) {
463
				$search_criteria['field_filters'][] = array(
464
					'key' => null, // The field ID to search
465
					'value' => $word, // The value to search
466
					'operator' => 'contains', // What to search in. Options: `is` or `contains`
467
				);
468
			}
469
		}
470
471
		//start date & end date
472
		$curr_start = !empty( $get['gv_start'] ) ? $get['gv_start'] : '';
473
		$curr_end = !empty( $get['gv_start'] ) ? $get['gv_end'] : '';
474
475
        /**
476
         * @filter `gravityview_date_created_adjust_timezone` Whether to adjust the timezone for entries. \n
477
         * date_created is stored in UTC format. Convert search date into UTC (also used on templates/fields/date_created.php)
478
         * @since 1.12
479
         * @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
480
         * @param[in] string $context Where the filter is being called from. `search` in this case.
481
         */
482
        $adjust_tz = apply_filters( 'gravityview_date_created_adjust_timezone', true, 'search' );
483
0 ignored issues
show
Functions must not contain multiple empty lines in a row; found 2 empty lines
Loading history...
484
485
		/**
486
		 * Don't set $search_criteria['start_date'] if start_date is empty as it may lead to bad query results (GFAPI::get_entries)
487
		 */
488
		if( !empty( $curr_start ) ) {
489
			$search_criteria['start_date'] = $adjust_tz ? get_gmt_from_date( $curr_start ) : $curr_start;
490
		}
491
		if( !empty( $curr_end ) ) {
492
			$search_criteria['end_date'] = $adjust_tz ? get_gmt_from_date( $curr_end ) : $curr_end;
493
		}
494
495
		// search for a specific entry ID
496
		if ( ! empty( $get[ 'gv_id' ] ) ) {
497
			$search_criteria['field_filters'][] = array(
498
				'key' => 'id',
499
				'value' => absint( $get[ 'gv_id' ] ),
500
				'operator' => '=',
501
			);
502
		}
503
504
		// search for a specific Created_by ID
505
		if ( ! empty( $get[ 'gv_by' ] ) ) {
506
			$search_criteria['field_filters'][] = array(
507
				'key' => 'created_by',
508
				'value' => absint( $get['gv_by'] ),
509
				'operator' => '=',
510
			);
511
		}
512
0 ignored issues
show
Functions must not contain multiple empty lines in a row; found 2 empty lines
Loading history...
513
514
		// Get search mode passed in URL
515
		$mode = isset( $get['mode'] ) && in_array( $get['mode'], array( 'any', 'all' ) ) ?  $get['mode'] : 'any';
516
517
		// get the other search filters
518
		foreach ( $get as $key => $value ) {
519
520
			if ( 0 !== strpos( $key, 'filter_' ) || empty( $value ) || ( is_array( $value ) && count( $value ) === 1 && empty( $value[0] ) ) ) {
521
				continue;
522
			}
523
524
			// could return simple filter or multiple filters
525
			$filter = $this->prepare_field_filter( $key, $value );
526
527
			if ( isset( $filter[0]['value'] ) ) {
528
				$search_criteria['field_filters'] = array_merge( $search_criteria['field_filters'], $filter );
529
530
				// if date range type, set search mode to ALL
531
				if ( ! empty( $filter[0]['operator'] ) && in_array( $filter[0]['operator'], array( '>=', '<=', '>', '<' ) ) ) {
532
					$mode = 'all';
533
				}
534
			} elseif( !empty( $filter ) ) {
535
				$search_criteria['field_filters'][] = $filter;
536
			}
537
		}
538
539
		/**
540
		 * @filter `gravityview/search/mode` Set the Search Mode (`all` or `any`)
541
		 * @since 1.5.1
542
		 * @param[out,in] string $mode Search mode (`any` vs `all`)
543
		 */
544
		$search_criteria['field_filters']['mode'] = apply_filters( 'gravityview/search/mode', $mode );
545
546
		do_action( 'gravityview_log_debug', sprintf( '%s[filter_entries] Returned Search Criteria: ', get_class( $this ) ), $search_criteria );
547
548
		unset( $get );
549
550
		return $search_criteria;
551
	}
552
553
	/**
554
	 * Prepare the field filters to GFAPI
555
	 *
556
	 * 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.
557
	 *
558
	 * Format searched values
559
	 * @param  string $key   $_GET/$_POST search key
560
	 * @param  string $value $_GET/$_POST search value
561
	 * @return array        1 or 2 deph levels
562
	 */
563
	public function prepare_field_filter( $key, $value ) {
564
565
		$gravityview_view = GravityView_View::getInstance();
566
567
		$field_id = str_replace( 'filter_', '', $key );
568
569
		// calculates field_id, removing 'filter_' and for '_' for advanced fields ( like name or checkbox )
570
		if ( preg_match('/^[0-9_]+$/ism', $field_id ) ) {
571
			$field_id = str_replace( '_', '.', $field_id );
572
		}
573
574
		// get form field array
575
		$form = $gravityview_view->getForm();
576
		$form_field = gravityview_get_field( $form, $field_id );
577
		
578
		// default filter array
579
		$filter = array(
580
			'key' => $field_id,
581
			'value' => $value,
582
		);
583
584
		switch ( $form_field['type'] ) {
585
586
			case 'select':
587
			case 'radio':
588
				$filter['operator'] = 'is';
589
				break;
590
591
			case 'post_category':
592
593
				if ( ! is_array( $value ) ) {
594
					$value = array( $value );
595
				}
596
597
				// Reset filter variable
598
				$filter = array();
599
600
				foreach ( $value as $val ) {
601
					$cat = get_term( $val, 'category' );
602
					$filter[] = array(
603
						'key' => $field_id,
604
						'value' => esc_attr( $cat->name ) . ':' . $val,
605
						'operator' => 'is',
606
					);
607
				}
608
609
				break;
610
611
			case 'multiselect':
612
613
				if ( ! is_array( $value ) ) {
614
					break;
615
				}
616
617
				// Reset filter variable
618
				$filter = array();
619
620
				foreach ( $value as $val ) {
621
					$filter[] = array( 'key' => $field_id, 'value' => $val );
622
				}
623
624
				break;
625
626
			case 'checkbox':
627
				// convert checkbox on/off into the correct search filter
628
				if ( false !== strpos( $field_id, '.' ) && ! empty( $form_field['inputs'] ) && ! empty( $form_field['choices'] ) ) {
629
					foreach ( $form_field['inputs'] as $k => $input ) {
630
						if ( $input['id'] == $field_id ) {
631
							$filter['value'] = $form_field['choices'][ $k ]['value'];
632
							$filter['operator'] = 'is';
633
							break;
634
						}
635
					}
636
				} elseif ( is_array( $value ) ) {
637
638
					// Reset filter variable
639
					$filter = array();
640
641
					foreach ( $value as $val ) {
642
						$filter[] = array(
643
								'key'   => $field_id,
644
								'value' => $val,
645
								'operator' => 'is',
646
						);
647
					}
648
				}
649
650
				break;
651
652
			case 'name':
653
			case 'address':
654
655
				if ( false === strpos( $field_id, '.' ) ) {
656
657
					$words = explode( ' ', $value );
658
659
					$filters = array();
660
					foreach ( $words as $word ) {
661
						if ( ! empty( $word ) && strlen( $word ) > 1 ) {
662
							// Keep the same key for each filter
663
							$filter['value'] = $word;
664
							// Add a search for the value
665
							$filters[] = $filter;
666
						}
667
					}
668
669
					$filter = $filters;
670
				}
671
672
				break;
673
674
			case 'date':
675
676
				if ( is_array( $value ) ) {
677
678
					// Reset filter variable
679
					$filter = array();
680
681
					foreach ( $value as $k => $date ) {
682
						if ( empty( $date ) ) {
683
							continue;
684
						}
685
						$operator = 'start' === $k ? '>=' : '<=';
686
687
						/**
688
						 * @hack
689
						 * @since 1.16.3
690
						 * Safeguard until GF implements '<=' operator
691
						 */
692
						if( !GFFormsModel::is_valid_operator( $operator ) && $operator === '<=' ) {
693
							$operator = '<';
694
							$date = date( 'Y-m-d', strtotime( $date . ' +1 day' ) );
695
						}
696
697
						$filter[] = array(
698
							'key' => $field_id,
699
							'value' => self::get_formatted_date( $date, 'Y-m-d' ),
700
							'operator' => $operator,
701
						);
702
					}
703
				} else {
704
					$filter['value'] = self::get_formatted_date( $value, 'Y-m-d' );
705
				}
706
707
				break;
708
0 ignored issues
show
Functions must not contain multiple empty lines in a row; found 2 empty lines
Loading history...
709
710
		} // switch field type
711
712
		return $filter;
713
	}
714
715
	/**
716
	 * Get the Field Format form GravityForms
717
	 *
718
	 * @param GF_Field_Date $field The field object
719
	 * @since 1.10
720
	 *
721
	 * @return string Format of the date in the database
722
	 */
723
	public static function get_date_field_format( GF_Field_Date $field ) {
724
		$format = 'm/d/Y';
725
		$datepicker = array(
726
			'mdy' => 'm/d/Y',
727
			'dmy' => 'd/m/Y',
728
			'dmy_dash' => 'd-m-Y',
729
			'dmy_dot' => 'm.d.Y',
730
			'ymd_slash' => 'Y/m/d',
731
			'ymd_dash' => 'Y-m-d',
732
			'ymd_dot' => 'Y.m.d',
733
		);
734
735
		if ( ! empty( $field->dateFormat ) && isset( $datepicker[ $field->dateFormat ] ) ){
736
			$format = $datepicker[ $field->dateFormat ];
737
		}
738
739
		return $format;
740
	}
741
742
	/**
743
	 * Format a date value
744
	 *
745
	 * @param string $value Date value input
746
	 * @param string $format Wanted formatted date
747
	 * @return string
748
	 */
749
	public static function get_formatted_date( $value = '', $format = 'Y-m-d' ) {
750
		$date = date_create( $value );
751
		if ( empty( $date ) ) {
752
			do_action( 'gravityview_log_debug', sprintf( '%s[get_formatted_date] Date format not valid: ', get_class( self::$instance ) ), $value );
753
			return '';
754
		}
755
		return $date->format( $format );
756
	}
757
758
759
	/**
760
	 * Include this extension templates path
761
	 * @param array $file_paths List of template paths ordered
762
	 */
763
	public function add_template_path( $file_paths ) {
764
765
		// Index 100 is the default GravityView template path.
766
		$file_paths[102] = self::$file . 'templates/';
767
768
		return $file_paths;
769
	}
770
771
	/**
772
	 * Check whether the configured search fields have a date field
773
	 *
774
	 * @since 1.17.5
775
	 *
776
	 * @param array $search_fields
777
	 *
778
	 * @return bool True: has a `date` or `date_range` field
779
	 */
780
	private function has_date_field( $search_fields ) {
781
782
		$has_date = false;
783
784
		foreach ( $search_fields as $k => $field ) {
785
			if ( in_array( $field['input'], array( 'date', 'date_range', 'entry_date' ) ) ) {
786
				$has_date = true;
787
				break;
788
			}
789
		}
790
791
		return $has_date;
792
	}
793
794
	/**
795
	 * Renders the Search Widget
796
	 * @param array $widget_args
797
	 * @param string $content
798
	 * @param string $context
799
	 *
800
	 * @return void
801
	 */
802
	public function render_frontend( $widget_args, $content = '', $context = '' ) {
803
		/** @var GravityView_View $gravityview_view */
804
		$gravityview_view = GravityView_View::getInstance();
805
806
		if ( empty( $gravityview_view ) ) {
807
			do_action( 'gravityview_log_debug', sprintf( '%s[render_frontend]: $gravityview_view not instantiated yet.', get_class( $this ) ) );
808
			return;
809
		}
810
811
		// get configured search fields
812
		$search_fields = ! empty( $widget_args['search_fields'] ) ? json_decode( $widget_args['search_fields'], true ) : '';
813
814
		if ( empty( $search_fields ) || ! is_array( $search_fields ) ) {
815
			do_action( 'gravityview_log_debug', sprintf( '%s[render_frontend] No search fields configured for widget:', get_class( $this ) ), $widget_args );
816
			return;
817
		}
818
0 ignored issues
show
Functions must not contain multiple empty lines in a row; found 2 empty lines
Loading history...
819
820
		// prepare fields
821
		foreach ( $search_fields as $k => $field ) {
822
823
			$updated_field = $field;
824
825
			$updated_field = $this->get_search_filter_details( $updated_field );
826
827
			switch ( $field['field'] ) {
828
829
				case 'search_all':
830
					$updated_field['key'] = 'search_all';
831
					$updated_field['input'] = 'search_all';
832
					$updated_field['value'] = $this->rgget_or_rgpost( 'gv_search' );
833
					break;
834
835
				case 'entry_date':
836
					$updated_field['key'] = 'entry_date';
837
					$updated_field['input'] = 'entry_date';
838
					$updated_field['value'] = array(
839
						'start' => $this->rgget_or_rgpost( 'gv_start' ),
840
						'end' => $this->rgget_or_rgpost( 'gv_end' ),
841
					);
842
					break;
843
844
				case 'entry_id':
845
					$updated_field['key'] = 'entry_id';
846
					$updated_field['input'] = 'entry_id';
847
					$updated_field['value'] = $this->rgget_or_rgpost( 'gv_id' );
848
					break;
849
850
				case 'created_by':
851
					$updated_field['key'] = 'created_by';
852
					$updated_field['name'] = 'gv_by';
853
					$updated_field['value'] = $this->rgget_or_rgpost( 'gv_by' );
854
					$updated_field['choices'] = self::get_created_by_choices();
855
					break;
856
			}
857
858
			$search_fields[ $k ] = $updated_field;
859
		}
860
861
		do_action( 'gravityview_log_debug', sprintf( '%s[render_frontend] Calculated Search Fields: ', get_class( $this ) ), $search_fields );
862
863
		/**
864
		 * @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.
865
		 * @param array $search_fields Array of search filters with `key`, `label`, `value`, `type` keys
866
		 * @param GravityView_Widget_Search $this Current widget object
867
		 * @param array $widget_args Args passed to this method. {@since 1.8}
868
		 * @var array
869
		 */
870
		$gravityview_view->search_fields = apply_filters( 'gravityview_widget_search_filters', $search_fields, $this, $widget_args );
871
872
		$gravityview_view->search_layout = ! empty( $widget_args['search_layout'] ) ? $widget_args['search_layout'] : 'horizontal';
873
874
		/** @since 1.14 */
875
		$gravityview_view->search_mode = ! empty( $widget_args['search_mode'] ) ? $widget_args['search_mode'] : 'any';
876
877
		$custom_class = ! empty( $widget_args['custom_class'] ) ? $widget_args['custom_class'] : '';
878
879
		$gravityview_view->search_class = self::get_search_class( $custom_class );
880
881
		$gravityview_view->search_clear = ! empty( $widget_args['search_clear'] ) ? $widget_args['search_clear'] : false;
882
883
		if ( $this->has_date_field( $search_fields ) ) {
884
			// enqueue datepicker stuff only if needed!
885
			$this->enqueue_datepicker();
886
		}
887
888
		$this->maybe_enqueue_flexibility();
889
890
		$gravityview_view->render( 'widget', 'search', false );
891
	}
892
893
	/**
894
	 * Get the search class for a search form
895
	 *
896
	 * @since 1.5.4
897
	 *
898
	 * @return string Sanitized CSS class for the search form
899
	 */
900
	public static function get_search_class( $custom_class = '' ) {
901
		$gravityview_view = GravityView_View::getInstance();
902
903
		$search_class = 'gv-search-'.$gravityview_view->search_layout;
904
905
		if ( ! empty( $custom_class )  ) {
906
			$search_class .= ' '.$custom_class;
907
		}
908
909
		/**
910
		 * @filter `gravityview_search_class` Modify the CSS class for the search form
911
		 * @param string $search_class The CSS class for the search form
912
		 */
913
		$search_class = apply_filters( 'gravityview_search_class', $search_class );
914
915
		// Is there an active search being performed? Used by fe-views.js
916
		$search_class .= GravityView_frontend::getInstance()->isSearch() ? ' gv-is-search' : '';
917
918
		return gravityview_sanitize_html_class( $search_class );
919
	}
920
921
922
	/**
923
	 * Calculate the search form action
924
	 * @since 1.6
925
	 *
926
	 * @return string
927
	 */
928
	public static function get_search_form_action() {
929
		$gravityview_view = GravityView_View::getInstance();
930
931
		$post_id = $gravityview_view->getPostId() ? $gravityview_view->getPostId() : $gravityview_view->getViewId();
932
933
		$url = add_query_arg( array(), get_permalink( $post_id ) );
934
935
		return esc_url( $url );
936
	}
937
938
	/**
939
	 * Get the label for a search form field
940
	 * @param  array $field      Field setting as sent by the GV configuration - has `field`, `input` (input type), and `label` keys
941
	 * @param  array $form_field Form field data, as fetched by `gravityview_get_field()`
942
	 * @return string             Label for the search form
943
	 */
944
	private static function get_field_label( $field, $form_field = array() ) {
945
946
		$label = rgget( 'label', $field );
947
948
		if( '' === $label ) {
949
950
			$label = isset( $form_field['label'] ) ? $form_field['label'] : '';
951
952
			switch( $field['field'] ) {
953
				case 'search_all':
954
					$label = __( 'Search Entries:', 'gravityview' );
955
					break;
956
				case 'entry_date':
957
					$label = __( 'Filter by date:', 'gravityview' );
958
					break;
959
				case 'entry_id':
960
					$label = __( 'Entry ID:', 'gravityview' );
961
					break;
962
				default:
963
					// If this is a field input, not a field
964
					if ( strpos( $field['field'], '.' ) > 0 && ! empty( $form_field['inputs'] ) ) {
965
966
						// Get the label for the field in question, which returns an array
967
						$items = wp_list_filter( $form_field['inputs'], array( 'id' => $field['field'] ) );
968
969
						// Get the item with the `label` key
970
						$values = wp_list_pluck( $items, 'label' );
971
972
						// There will only one item in the array, but this is easier
973
						foreach ( $values as $value ) {
974
							$label = $value;
975
							break;
976
						}
977
					}
978
			}
979
		}
980
981
		/**
982
		 * @filter `gravityview_search_field_label` Modify the label for a search field. Supports returning HTML
983
		 * @since 1.17.3 Added $field parameter
984
		 * @param[in,out] string $label Existing label text, sanitized.
985
		 * @param[in] array $form_field Gravity Forms field array, as returned by `GFFormsModel::get_field()`
986
		 * @param[in] array $field Field setting as sent by the GV configuration - has `field`, `input` (input type), and `label` keys
987
		 */
988
		$label = apply_filters( 'gravityview_search_field_label', esc_attr( $label ), $form_field, $field );
989
990
		return $label;
991
	}
992
993
	/**
994
	 * Prepare search fields to frontend render with other details (label, field type, searched values)
995
	 *
996
	 * @param array $field
997
	 * @return array
998
	 */
999
	private function get_search_filter_details( $field ) {
1000
1001
		$gravityview_view = GravityView_View::getInstance();
1002
1003
		$form = $gravityview_view->getForm();
1004
1005
		// for advanced field ids (eg, first name / last name )
1006
		$name = 'filter_' . str_replace( '.', '_', $field['field'] );
1007
1008
		// get searched value from $_GET/$_POST (string or array)
1009
		$value = $this->rgget_or_rgpost( $name );
1010
1011
		// get form field details
1012
		$form_field = gravityview_get_field( $form, $field['field'] );
1013
1014
		$filter = array(
1015
			'key' => $field['field'],
1016
			'name' => $name,
1017
			'label' => self::get_field_label( $field, $form_field ),
1018
			'input' => $field['input'],
1019
			'value' => $value,
1020
			'type' => $form_field['type'],
1021
		);
1022
1023
		// collect choices
1024
		if ( 'post_category' === $form_field['type'] && ! empty( $form_field['displayAllCategories'] ) && empty( $form_field['choices'] ) ) {
1025
			$filter['choices'] = gravityview_get_terms_choices();
1026
		} elseif ( ! empty( $form_field['choices'] ) ) {
1027
			$filter['choices'] = $form_field['choices'];
1028
		}
1029
1030
		if ( 'date_range' === $field['input'] && empty( $value ) ) {
1031
			$filter['value'] = array( 'start' => '', 'end' => '' );
1032
		}
1033
1034
		return $filter;
1035
1036
	}
1037
1038
	/**
1039
	 * Calculate the search choices for the users
1040
	 *
1041
	 * @since 1.8
1042
	 *
1043
	 * @return array Array of user choices (value = ID, text = display name)
1044
	 */
1045
	private static function get_created_by_choices() {
1046
1047
		/**
1048
		 * filter gravityview/get_users/search_widget
1049
		 * @see \GVCommon::get_users
1050
		 */
1051
		$users = GVCommon::get_users( 'search_widget', array( 'fields' => array( 'ID', 'display_name' ) ) );
1052
1053
		$choices = array();
1054
		foreach ( $users as $user ) {
1055
			$choices[] = array(
1056
				'value' => $user->ID,
1057
				'text' => $user->display_name,
1058
			);
1059
		}
1060
1061
		return $choices;
1062
	}
1063
1064
1065
	/**
1066
	 * Output the Clear Search Results button
1067
	 * @since 1.5.4
1068
	 */
1069
	public static function the_clear_search_button() {
1070
		$gravityview_view = GravityView_View::getInstance();
1071
1072
		if ( $gravityview_view->search_clear ) {
1073
1074
			$url = strtok( add_query_arg( array() ), '?' );
1075
1076
			echo gravityview_get_link( $url, esc_html__( 'Clear', 'gravityview' ), 'class=button gv-search-clear' );
1077
1078
		}
1079
	}
1080
1081
	/**
1082
	 * Based on the search method, fetch the value for a specific key
1083
	 * 
1084
	 * @since 1.16.4
1085
	 *
1086
	 * @param string $name Name of the request key to fetch the value for
1087
	 *
1088
	 * @return mixed|string Value of request at $name key. Empty string if empty.
1089
	 */
1090
	private function rgget_or_rgpost( $name ) {
1091
		$value = 'get' === $this->search_method ? rgget( $name ) : rgpost( $name );
1092
1093
		$value = stripslashes_deep( $value );
1094
1095
		$value = gv_map_deep( $value, 'rawurldecode' );
1096
1097
		$value = gv_map_deep( $value, '_wp_specialchars' );
1098
1099
		return $value;
1100
	}
1101
1102
1103
	/**
1104
	 * Require the datepicker script for the frontend GV script
1105
	 * @param array $js_dependencies Array of existing required scripts for the fe-views.js script
1106
	 * @return array Array required scripts, with `jquery-ui-datepicker` added
1107
	 */
1108
	public function add_datepicker_js_dependency( $js_dependencies ) {
1109
1110
		$js_dependencies[] = 'jquery-ui-datepicker';
1111
1112
		return $js_dependencies;
1113
	}
1114
1115
	/**
1116
	 * Modify the array passed to wp_localize_script()
1117
	 *
1118
	 * @param array $js_localization The data padded to the Javascript file
1119
	 * @param array $view_data View data array with View settings
1120
	 *
1121
	 * @return array
1122
	 */
1123
	public function add_datepicker_localization( $localizations = array(), $view_data = array() ) {
1124
		global $wp_locale;
1125
1126
		/**
1127
		 * @filter `gravityview_datepicker_settings` Modify the datepicker settings
1128
		 * @see http://api.jqueryui.com/datepicker/ Learn what settings are available
1129
		 * @see http://www.renegadetechconsulting.com/tutorials/jquery-datepicker-and-wordpress-i18n Thanks for the helpful information on $wp_locale
1130
		 * @param array $js_localization The data padded to the Javascript file
1131
		 * @param array $view_data View data array with View settings
1132
		 */
1133
		$datepicker_settings = apply_filters( 'gravityview_datepicker_settings', array(
1134
			'yearRange' => '-5:+5',
1135
			'changeMonth' => true,
1136
			'changeYear' => true,
1137
			'closeText' => esc_attr_x( 'Close', 'Close calendar', 'gravityview' ),
1138
			'prevText' => esc_attr_x( 'Prev', 'Previous month in calendar', 'gravityview' ),
1139
			'nextText' => esc_attr_x( 'Next', 'Next month in calendar', 'gravityview' ),
1140
			'currentText' => esc_attr_x( 'Today', 'Today in calendar', 'gravityview' ),
1141
			'weekHeader' => esc_attr_x( 'Week', 'Week in calendar', 'gravityview' ),
1142
			'monthStatus'       => __( 'Show a different month', 'gravityview' ),
1143
			'monthNames'        => array_values( $wp_locale->month ),
1144
			'monthNamesShort'   => array_values( $wp_locale->month_abbrev ),
1145
			'dayNames'          => array_values( $wp_locale->weekday ),
1146
			'dayNamesShort'     => array_values( $wp_locale->weekday_abbrev ),
1147
			'dayNamesMin'       => array_values( $wp_locale->weekday_initial ),
1148
			// get the start of week from WP general setting
1149
			'firstDay'          => get_option( 'start_of_week' ),
1150
			// is Right to left language? default is false
1151
			'isRTL'             => is_rtl(),
1152
		), $view_data );
1153
1154
		$localizations['datepicker'] = $datepicker_settings;
1155
1156
		return $localizations;
1157
1158
	}
1159
1160
	/**
1161
	 * Register search widget scripts, including Flexibility
1162
	 *
1163
	 * @see https://github.com/10up/flexibility
1164
	 *
1165
	 * @since 1.17
1166
	 *
1167
	 * @return void
1168
	 */
1169
	public function register_scripts() {
1170
1171
		wp_register_script( 'gv-flexibility', plugins_url( 'assets/lib/flexibility/dist/flexibility.js', GRAVITYVIEW_FILE ), array(), GravityView_Plugin::version, true );
1172
1173
	}
1174
1175
	/**
1176
	 * If the current visitor is running IE 8 or 9, enqueue Flexibility
1177
	 *
1178
	 * @since 1.17
1179
	 *
1180
	 * @return void
1181
	 */
1182
	private function maybe_enqueue_flexibility() {
1183
		if ( isset( $_SERVER['HTTP_USER_AGENT'] ) && preg_match( '/MSIE [8-9]/', $_SERVER['HTTP_USER_AGENT'] ) ) {
1184
			wp_enqueue_script( 'gv-flexibility' );
1185
		}
1186
	}
1187
1188
	/**
1189
	 * Enqueue the datepicker script
1190
	 *
1191
	 * It sets the $gravityview->datepicker_class parameter
1192
	 *
1193
	 * @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.
1194
	 * @return void
1195
	 */
1196
	public function enqueue_datepicker() {
1197
		$gravityview_view = GravityView_View::getInstance();
1198
1199
		wp_enqueue_script( 'jquery-ui-datepicker' );
1200
1201
		add_filter( 'gravityview_js_dependencies', array( $this, 'add_datepicker_js_dependency' ) );
1202
		add_filter( 'gravityview_js_localization', array( $this, 'add_datepicker_localization' ), 10, 2 );
1203
1204
		$scheme = is_ssl() ? 'https://' : 'http://';
1205
		wp_enqueue_style( 'jquery-ui-datepicker', $scheme.'ajax.googleapis.com/ajax/libs/jqueryui/1.8.18/themes/smoothness/jquery-ui.css' );
1206
1207
		/**
1208
		 * @filter `gravityview_search_datepicker_class`
1209
		 * 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.
1210
		 * @param string $css_class CSS class to use. Default: `gv-datepicker datepicker mdy` \n
1211
		 * Options are:
1212
		 * - `mdy` (mm/dd/yyyy)
1213
		 * - `dmy` (dd/mm/yyyy)
1214
		 * - `dmy_dash` (dd-mm-yyyy)
1215
		 * - `dmy_dot` (dd.mm.yyyy)
1216
		 * - `ymp_slash` (yyyy/mm/dd)
1217
		 * - `ymd_dash` (yyyy-mm-dd)
1218
		 * - `ymp_dot` (yyyy.mm.dd)
1219
		 */
1220
		$datepicker_class = apply_filters( 'gravityview_search_datepicker_class', 'gv-datepicker datepicker mdy' );
1221
1222
		$gravityview_view->datepicker_class = $datepicker_class;
1223
1224
	}
1225
1226
1227
} // end class
1228
1229
new GravityView_Widget_Search;