GravityView  2.17
The best, easiest way to display Gravity Forms entries on your website.
class-search-widget.php
Go to the documentation of this file.
1 <?php
2 /**
3  * The GravityView New Search widget
4  *
5  * @package GravityView-DataTables-Ext
6  * @license GPL2+
7  * @author GravityView <[email protected]>
8  * @link http://gravityview.co
9  * @copyright Copyright 2014, Katz Web Services, Inc.
10  */
11 
12 if ( ! defined( 'WPINC' ) ) {
13  die;
14 }
15 
17 
18  public $icon = 'dashicons-search';
19 
20  public static $file;
21  public static $instance;
22 
23  private $search_filters = array();
24 
25  /**
26  * whether search method is GET or POST ( default: GET )
27  * @since 1.16.4
28  * @var string $search_method
29  */
30  private $search_method = 'get';
31 
32  public function __construct() {
33 
34  $this->widget_id = 'search_bar';
35  $this->widget_description = '';
36  $this->widget_subtitle = esc_html__( 'Search form for searching entries.', 'gk-gravityview' );
37 
38  self::$instance = &$this;
39 
40  self::$file = plugin_dir_path( __FILE__ );
41 
42  $default_values = array( 'header' => 0, 'footer' => 0 );
43 
44  $settings = array(
45  'search_layout' => array(
46  'type' => 'radio',
47  'full_width' => true,
48  'label' => esc_html__( 'Search Layout', 'gk-gravityview' ),
49  'value' => 'horizontal',
50  'options' => array(
51  'horizontal' => esc_html__( 'Horizontal', 'gk-gravityview' ),
52  'vertical' => esc_html__( 'Vertical', 'gk-gravityview' ),
53  ),
54  ),
55  'search_clear' => array(
56  'type' => 'checkbox',
57  'label' => __( 'Show Clear button', 'gk-gravityview' ),
58  'desc' => __( 'When a search is performed, display a button that removes all search values.', 'gk-gravityview'),
59  'value' => true,
60  ),
61  'search_fields' => array(
62  'type' => 'hidden',
63  'label' => '',
64  'class' => 'gv-search-fields-value',
65  'value' => '[{"field":"search_all","input":"input_text"}]', // Default: Search Everything text box
66  ),
67  'search_mode' => array(
68  'type' => 'radio',
69  'full_width' => true,
70  'label' => esc_html__( 'Search Mode', 'gk-gravityview' ),
71  'desc' => __('Should search results match all search fields, or any?', 'gk-gravityview'),
72  'value' => 'any',
73  'class' => 'hide-if-js',
74  'options' => array(
75  'any' => esc_html__( 'Match Any Fields', 'gk-gravityview' ),
76  'all' => esc_html__( 'Match All Fields', 'gk-gravityview' ),
77  ),
78  ),
79  'sieve_choices' => array(
80  'type' => 'radio',
81  'full_width' => true,
82  'label' => esc_html__( 'Pre-Filter Choices', 'gk-gravityview' ),
83  // translators: Do not translate [b], [/b], [link], or [/link]; they are placeholders for HTML and links to documentation.
84  'desc' => strtr(
85  esc_html__( 'For fields with choices: Instead of showing all choices for each field, show only field choices that exist in submitted form entries.', 'gk-gravityview' ) .
86  '<p><strong>⚠️ ' . esc_html__('This setting affects security.', 'gk-gravityview' ) . '</strong> ' . esc_html__( '[link]Learn about the Pre-Filter Choices setting[/link] before enabling it.', 'gk-gravityview') . '</p>',
87  array(
88  '[b]' => '<strong>',
89  '[/b]' => '</strong>',
90  '[link]' => '<a href="https://docs.gravitykit.com/article/701-s" target="_blank" rel="external noopener nofollower" title="' . esc_attr__( 'This link opens in a new window.', 'gk-gravityview' ) . '">',
91  '[/link]' => '</a>',
92  )
93  ),
94  'value' => '0',
95  'class' => 'hide-if-js',
96  'options' => array(
97  '0' => esc_html__( 'Show all field choices', 'gk-gravityview' ),
98  '1' => esc_html__( 'Only show choices that exist in form entries', 'gk-gravityview' ),
99  ),
100  ),
101  );
102 
103  if ( ! $this->is_registered() ) {
104  // frontend - filter entries
105  add_filter( 'gravityview_fe_search_criteria', array( $this, 'filter_entries' ), 10, 3 );
106 
107  // frontend - add template path
108  add_filter( 'gravityview_template_paths', array( $this, 'add_template_path' ) );
109 
110  // admin - add scripts - run at 1100 to make sure GravityView_Admin_Views::add_scripts_and_styles() runs first at 999
111  add_action( 'admin_enqueue_scripts', array( $this, 'add_scripts_and_styles' ), 1100 );
112  add_action( 'wp_enqueue_scripts', array( $this, 'register_scripts') );
113  add_filter( 'gravityview_noconflict_scripts', array( $this, 'register_no_conflict' ) );
114 
115  // ajax - get the searchable fields
116  add_action( 'wp_ajax_gv_searchable_fields', array( 'GravityView_Widget_Search', 'get_searchable_fields' ) );
117 
118  add_action( 'gravityview_search_widget_fields_after', array( $this, 'add_preview_inputs' ) );
119 
120  add_filter( 'gravityview/api/reserved_query_args', array( $this, 'add_reserved_args' ) );
121 
122  // All other GravityView-added hooks for this filter run at the default 10.
123  add_filter( 'gravityview_widget_search_filters', array( $this, 'maybe_sieve_filter_choices' ), 1000, 4 );
124  }
125 
126  parent::__construct( esc_html__( 'Search Bar', 'gk-gravityview' ), null, $default_values, $settings );
127 
128  // calculate the search method (POST / GET)
129  $this->set_search_method();
130  }
131 
132  /**
133  * @return GravityView_Widget_Search
134  */
135  public static function getInstance() {
136  if ( empty( self::$instance ) ) {
137  self::$instance = new GravityView_Widget_Search;
138  }
139  return self::$instance;
140  }
141 
142  /**
143  * @since 2.10
144  *
145  * @param $args
146  *
147  * @return mixed
148  */
149  public function add_reserved_args( $args ) {
150 
151  $args[] = 'gv_search';
152  $args[] = 'gv_start';
153  $args[] = 'gv_end';
154  $args[] = 'gv_id';
155  $args[] = 'gv_by';
156  $args[] = 'mode';
157 
158  $get = (array) $_GET;
159 
160  // If the fields being searched as reserved; not to be considered user-passed variables
161  foreach ( $get as $key => $value ) {
162  if ( $key !== $this->convert_request_key_to_filter_key( $key ) ) {
163  $args[] = $key;
164  }
165  }
166 
167  return $args;
168  }
169 
170  /**
171  * Sets the search method to GET (default) or POST
172  * @since 1.16.4
173  */
174  private function set_search_method() {
175  /**
176  * @filter `gravityview/search/method` Modify the search form method (GET / POST)
177  * @since 1.16.4
178  * @param string $search_method Assign an input type according to the form field type. Defaults: `boolean`, `multi`, `select`, `date`, `text`
179  * @param string $field_type Gravity Forms field type (also the `name` parameter of GravityView_Field classes)
180  */
181  $method = apply_filters( 'gravityview/search/method', $this->search_method );
182 
183  $method = strtolower( $method );
184 
185  $this->search_method = in_array( $method, array( 'get', 'post' ) ) ? $method : 'get';
186  }
187 
188  /**
189  * Returns the search method
190  * @since 1.16.4
191  * @return string
192  */
193  public function get_search_method() {
194  return $this->search_method;
195  }
196 
197  /**
198  * Get the input types available for different field types
199  *
200  * @since 1.17.5
201  *
202  * @return array [field type name] => (array|string) search bar input types
203  */
204  public static function get_input_types_by_field_type() {
205  /**
206  * Input Type groups
207  * @see admin-search-widget.js (getSelectInput)
208  */
209  $input_types = array(
210  'text' => array( 'input_text' ),
211  'address' => array( 'input_text' ),
212  'number' => array( 'input_text' ),
213  'date' => array( 'date', 'date_range' ),
214  'boolean' => array( 'single_checkbox' ),
215  'select' => array( 'select', 'radio', 'link' ),
216  'multi' => array( 'select', 'multiselect', 'radio', 'checkbox', 'link' ),
217 
218  // hybrids
219  'created_by' => array( 'select', 'radio', 'checkbox', 'multiselect', 'link', 'input_text' ),
220  'product' => array( 'select', 'radio', 'link', 'input_text' ),
221  );
222 
223  /**
224  * @filter `gravityview/search/input_types` Change the types of search fields available to a field type
225  * @see GravityView_Widget_Search::get_search_input_labels() for the available input types
226  * @param array $input_types Associative array: key is field `name`, value is array of GravityView input types (note: use `input_text` for `text`)
227  */
228  $input_types = apply_filters( 'gravityview/search/input_types', $input_types );
229 
230  return $input_types;
231  }
232 
233  /**
234  * Get labels for different types of search bar inputs
235  *
236  * @since 1.17.5
237  *
238  * @return array [input type] => input type label
239  */
240  public static function get_search_input_labels() {
241  /**
242  * Input Type labels l10n
243  * @see admin-search-widget.js (getSelectInput)
244  */
245  $input_labels = array(
246  'input_text' => esc_html__( 'Text', 'gk-gravityview' ),
247  'date' => esc_html__( 'Date', 'gk-gravityview' ),
248  'select' => esc_html__( 'Select', 'gk-gravityview' ),
249  'multiselect' => esc_html__( 'Select (multiple values)', 'gk-gravityview' ),
250  'radio' => esc_html__( 'Radio', 'gk-gravityview' ),
251  'checkbox' => esc_html__( 'Checkbox', 'gk-gravityview' ),
252  'single_checkbox' => esc_html__( 'Checkbox', 'gk-gravityview' ),
253  'link' => esc_html__( 'Links', 'gk-gravityview' ),
254  'date_range' => esc_html__( 'Date range', 'gk-gravityview' ),
255  );
256 
257  /**
258  * @filter `gravityview/search/input_types` Change the label of search field input types
259  * @param array $input_types Associative array: key is input type name, value is label
260  */
261  $input_labels = apply_filters( 'gravityview/search/input_labels', $input_labels );
262 
263  return $input_labels;
264  }
265 
266  public static function get_search_input_label( $input_type ) {
267  $labels = self::get_search_input_labels();
268 
269  return \GV\Utils::get( $labels, $input_type, false );
270  }
271 
272  /**
273  * Add script to Views edit screen (admin)
274  * @param mixed $hook
275  */
276  public function add_scripts_and_styles( $hook ) {
277  global $pagenow;
278 
279  // Don't process any scripts below here if it's not a GravityView page or the widgets screen
280  if ( ! gravityview()->request->is_admin( $hook, 'single' ) && ( 'widgets.php' !== $pagenow ) ) {
281  return;
282  }
283 
284  $script_min = ( defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ) ? '' : '.min';
285  $script_source = empty( $script_min ) ? '/source' : '';
286 
287  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 );
288 
289  wp_localize_script( 'gravityview_searchwidget_admin', 'gvSearchVar', array(
290  'nonce' => wp_create_nonce( 'gravityview_ajaxsearchwidget' ),
291  'label_nofields' => esc_html__( 'No search fields configured yet.', 'gk-gravityview' ),
292  'label_addfield' => esc_html__( 'Add Search Field', 'gk-gravityview' ),
293  'label_label' => esc_html__( 'Label', 'gk-gravityview' ),
294  'label_searchfield' => esc_html__( 'Search Field', 'gk-gravityview' ),
295  'label_inputtype' => esc_html__( 'Input Type', 'gk-gravityview' ),
296  'label_ajaxerror' => esc_html__( 'There was an error loading searchable fields. Save the View or refresh the page to fix this issue.', 'gk-gravityview' ),
297  'input_labels' => json_encode( self::get_search_input_labels() ),
298  'input_types' => json_encode( self::get_input_types_by_field_type() ),
299  ) );
300 
301  }
302 
303  /**
304  * Add admin script to the no-conflict scripts allowlist
305  * @param array $allowed Scripts allowed in no-conflict mode
306  * @return array Scripts allowed in no-conflict mode, plus the search widget script
307  */
308  public function register_no_conflict( $allowed ) {
309  $allowed[] = 'gravityview_searchwidget_admin';
310  return $allowed;
311  }
312 
313  /**
314  * Ajax
315  * Returns the form fields ( only the searchable ones )
316  *
317  * @return void
318  */
319  public static function get_searchable_fields() {
320 
321  if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( $_POST['nonce'], 'gravityview_ajaxsearchwidget' ) ) {
322  exit( '0' );
323  }
324 
325  $form = '';
326 
327  // Fetch the form for the current View
328  if ( ! empty( $_POST['view_id'] ) ) {
329 
330  $form = gravityview_get_form_id( $_POST['view_id'] );
331 
332  } elseif ( ! empty( $_POST['formid'] ) ) {
333 
334  $form = (int) $_POST['formid'];
335 
336  } elseif ( ! empty( $_POST['template_id'] ) && class_exists( 'GravityView_Ajax' ) ) {
337 
338  $form = GravityView_Ajax::pre_get_form_fields( $_POST['template_id'] );
339 
340  }
341 
342  // fetch form id assigned to the view
343  $response = self::render_searchable_fields( $form );
344 
345  exit( $response );
346  }
347 
348  /**
349  * Generates html for the available Search Fields dropdown
350  * @param int $form_id
351  * @param string $current (for future use)
352  * @return string
353  */
354  public static function render_searchable_fields( $form_id = null, $current = '' ) {
355 
356  if ( is_null( $form_id ) ) {
357  return '';
358  }
359 
360  // start building output
361 
362  $output = '<select class="gv-search-fields">';
363 
364  $custom_fields = array(
365  'search_all' => array(
366  'text' => esc_html__( 'Search Everything', 'gk-gravityview' ),
367  'type' => 'text',
368  ),
369  'entry_date' => array(
370  'text' => esc_html__( 'Entry Date', 'gk-gravityview' ),
371  'type' => 'date',
372  ),
373  'entry_id' => array(
374  'text' => esc_html__( 'Entry ID', 'gk-gravityview' ),
375  'type' => 'text',
376  ),
377  'created_by' => array(
378  'text' => esc_html__( 'Entry Creator', 'gk-gravityview' ),
379  'type' => 'created_by',
380  ),
381  'is_starred' => array(
382  'text' => esc_html__( 'Is Starred', 'gk-gravityview' ),
383  'type' => 'boolean',
384  ),
385  );
386 
387  if ( gravityview()->plugin->supports( \GV\Plugin::FEATURE_GFQUERY ) ) {
388  $custom_fields['is_approved'] = array(
389  'text' => esc_html__( 'Approval Status', 'gk-gravityview' ),
390  'type' => 'multi',
391  );
392  }
393 
394  foreach( $custom_fields as $custom_field_key => $custom_field ) {
395  $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'] );
396  }
397 
398  // Get fields with sub-inputs and no parent
399  $fields = gravityview_get_form_fields( $form_id, true, true );
400 
401  /**
402  * @filter `gravityview/search/searchable_fields` Modify the fields that are displayed as searchable in the Search Bar dropdown\n
403  * @since 1.17
404  * @see gravityview_get_form_fields() Used to fetch the fields
405  * @see GravityView_Widget_Search::get_search_input_types See this method to modify the type of input types allowed for a field
406  * @param array $fields Array of searchable fields, as fetched by gravityview_get_form_fields()
407  * @param int $form_id
408  */
409  $fields = apply_filters( 'gravityview/search/searchable_fields', $fields, $form_id );
410 
411  if ( ! empty( $fields ) ) {
412 
413  $blocklist_field_types = apply_filters( 'gravityview_blocklist_field_types', array( 'fileupload', 'post_image', 'post_id', 'section' ), null );
414 
415  foreach ( $fields as $id => $field ) {
416 
417  if ( in_array( $field['type'], $blocklist_field_types ) ) {
418  continue;
419  }
420 
421  $types = self::get_search_input_types( $id, $field['type'] );
422 
423  $output .= '<option value="'. $id .'" '. selected( $id, $current, false ).'data-inputtypes="'. esc_attr( $types ) .'">'. esc_html( $field['label'] ) .'</option>';
424  }
425  }
426 
427  $output .= '</select>';
428 
429  return $output;
430 
431  }
432 
433  /**
434  * Assign an input type according to the form field type
435  *
436  * @see admin-search-widget.js
437  *
438  * @param string|int|float $field_id Gravity Forms field ID
439  * @param string $field_type Gravity Forms field type (also the `name` parameter of GravityView_Field classes)
440  *
441  * @return string GV field search input type ('multi', 'boolean', 'select', 'date', 'text')
442  */
443  public static function get_search_input_types( $field_id = '', $field_type = null ) {
444 
445  // @todo - This needs to be improved - many fields have . including products and addresses
446  if ( false !== strpos( (string) $field_id, '.' ) && in_array( $field_type, array( 'checkbox' ) ) || in_array( $field_id, array( 'is_fulfilled' ) ) ) {
447  $input_type = 'boolean'; // on/off checkbox
448  } elseif ( in_array( $field_type, array( 'checkbox', 'post_category', 'multiselect' ) ) ) {
449  $input_type = 'multi'; //multiselect
450  } elseif ( in_array( $field_type, array( 'select', 'radio' ) ) ) {
451  $input_type = 'select';
452  } elseif ( in_array( $field_type, array( 'date' ) ) || in_array( $field_id, array( 'payment_date' ) ) ) {
453  $input_type = 'date';
454  } elseif ( in_array( $field_type, array( 'number' ) ) || in_array( $field_id, array( 'payment_amount' ) ) ) {
455  $input_type = 'number';
456  } elseif ( in_array( $field_type, array( 'product' ) ) ) {
457  $input_type = 'product';
458  } else {
459  $input_type = 'text';
460  }
461 
462  /**
463  * @filter `gravityview/extension/search/input_type` Modify the search form input type based on field type
464  * @since 1.2
465  * @since 1.19.2 Added $field_id parameter
466  * @param string $input_type Assign an input type according to the form field type. Defaults: `boolean`, `multi`, `select`, `date`, `text`
467  * @param string $field_type Gravity Forms field type (also the `name` parameter of GravityView_Field classes)
468  * @param string|int|float $field_id ID of the field being processed
469  */
470  $input_type = apply_filters( 'gravityview/extension/search/input_type', $input_type, $field_type, $field_id );
471 
472  return $input_type;
473  }
474 
475  /**
476  * Display hidden fields to add support for sites using Default permalink structure
477  *
478  * @since 1.8
479  * @return array Search fields, modified if not using permalinks
480  */
481  public function add_no_permalink_fields( $search_fields, $object, $widget_args = array() ) {
482  /** @global WP_Rewrite $wp_rewrite */
483  global $wp_rewrite;
484 
485  // Support default permalink structure
486  if ( false === $wp_rewrite->using_permalinks() ) {
487 
488  // By default, use current post.
489  $post_id = 0;
490 
491  // We're in the WordPress Widget context, and an overriding post ID has been set.
492  if ( ! empty( $widget_args['post_id'] ) ) {
493  $post_id = absint( $widget_args['post_id'] );
494  }
495  // We're in the WordPress Widget context, and the base View ID should be used
496  else if ( ! empty( $widget_args['view_id'] ) ) {
497  $post_id = absint( $widget_args['view_id'] );
498  }
499 
501 
502  // Add hidden fields to the search form
503  foreach ( $args as $key => $value ) {
504  $search_fields[] = array(
505  'name' => $key,
506  'input' => 'hidden',
507  'value' => $value,
508  );
509  }
510  }
511 
512  return $search_fields;
513  }
514 
515  /**
516  * Get the fields that are searchable for a View
517  *
518  * @since 2.0
519  * @since 2.0.9 Added $with_full_field parameter
520  *
521  * @param \GV\View|null $view
522  * @param bool $with_full_field Return full field array, or just field ID? Default: false (just field ID)
523  *
524  * TODO: Move to \GV\View, perhaps? And return a Field_Collection
525  * TODO: Use in gravityview()->request->is_search() to calculate whether a valid search
526  *
527  * @return array If no View, returns empty array. Otherwise, returns array of fields configured in widgets and Search Bar for a View
528  */
529  private function get_view_searchable_fields( $view, $with_full_field = false ) {
530 
531  /**
532  * Find all search widgets on the view and get the searchable fields settings.
533  */
534  $searchable_fields = array();
535 
536  if ( ! $view ) {
537  return $searchable_fields;
538  }
539 
540  /**
541  * Include the sidebar Widgets.
542  */
543  $widgets = (array) get_option( 'widget_gravityview_search', array() );
544 
545  foreach ( $widgets as $widget ) {
546  if ( ! empty( $widget['view_id'] ) && $widget['view_id'] == $view->ID ) {
547  if( $_fields = json_decode( $widget['search_fields'], true ) ) {
548  foreach ( $_fields as $field ) {
549  if ( empty( $field['form_id'] ) ) {
550  $field['form_id'] = $view->form ? $view->form->ID : 0;
551  }
552  $searchable_fields[] = $with_full_field ? $field : $field['field'];
553  }
554  }
555  }
556  }
557 
558  foreach ( $view->widgets->by_id( $this->get_widget_id() )->all() as $widget ) {
559  if( $_fields = json_decode( $widget->configuration->get( 'search_fields' ), true ) ) {
560  foreach ( $_fields as $field ) {
561  if ( empty( $field['form_id'] ) ) {
562  $field['form_id'] = $view->form ? $view->form->ID : 0;
563  }
564  $searchable_fields[] = $with_full_field ? $field : $field['field'];
565  }
566  }
567  }
568 
569  /**
570  * @since 2.5.1
571  * @depecated 2.14
572  */
573  $searchable_fields = apply_filters_deprecated( 'gravityview/search/searchable_fields/whitelist', array( $searchable_fields, $view, $with_full_field ), '2.14', 'gravityview/search/searchable_fields/allowlist' );
574 
575  /**
576  * @filter `gravityview/search/searchable_fields/allowlist` Modifies the fields able to be searched using the Search Bar
577  * @since 2.14
578  *
579  * @param array $searchable_fields Array of GravityView-formatted fields or only the field ID? Example: [ '1.2', 'created_by' ]
580  * @param \GV\View $view Object of View being searched.
581  * @param bool $with_full_field Does $searchable_fields contain the full field array or just field ID? Default: false (just field ID)
582  */
583  $searchable_fields = apply_filters( 'gravityview/search/searchable_fields/allowlist', $searchable_fields, $view, $with_full_field );
584 
585  return $searchable_fields;
586  }
587 
588  /** --- Frontend --- */
589 
590  /**
591  * Calculate the search criteria to filter entries
592  * @param array $search_criteria The search criteria
593  * @param int $form_id The form ID
594  * @param array $args Some args
595  *
596  * @param bool $force_search_criteria Whether to suppress GF_Query filter, internally used in self::gf_query_filter
597  *
598  * @return array
599  */
600  public function filter_entries( $search_criteria, $form_id = null, $args = array(), $force_search_criteria = false ) {
601  if ( ! $force_search_criteria && gravityview()->plugin->supports( \GV\Plugin::FEATURE_GFQUERY ) ) {
602  /**
603  * If GF_Query is available, we can construct custom conditions with nested
604  * booleans on the query, giving up the old ways of flat search_criteria field_filters.
605  */
606  add_action( 'gravityview/view/query', array( $this, 'gf_query_filter' ), 10, 3 );
607  return $search_criteria; // Return the original criteria, GF_Query modification kicks in later
608  }
609 
610  if( 'post' === $this->search_method ) {
611  $get = $_POST;
612  } else {
613  $get = $_GET;
614  }
615 
616  $view = \GV\View::by_id( \GV\Utils::get( $args, 'id' ) );
617  $view_id = $view ? $view->ID : null;
618  $form_id = $view ? $view->form->ID : null;
619 
620  gravityview()->log->debug( 'Requested $_{method}: ', array( 'method' => $this->search_method, 'data' => $get ) );
621 
622  if ( empty( $get ) || ! is_array( $get ) ) {
623  return $search_criteria;
624  }
625 
626  $get = stripslashes_deep( $get );
627 
628  if ( ! is_null( $get ) ) {
629  $get = gv_map_deep( $get, 'rawurldecode' );
630  }
631 
632  // Make sure array key is set up
633  $search_criteria['field_filters'] = \GV\Utils::get( $search_criteria, 'field_filters', array() );
634 
635  $searchable_fields = $this->get_view_searchable_fields( $view );
636  $searchable_field_objects = $this->get_view_searchable_fields( $view, true );
637 
638  /**
639  * @filter `gravityview/search-all-split-words` Search for each word separately or the whole phrase?
640  * @since 1.20.2
641  * @param bool $split_words True: split a phrase into words; False: search whole word only [Default: true]
642  */
643  $split_words = apply_filters( 'gravityview/search-all-split-words', true );
644 
645  /**
646  * @filter `gravityview/search-trim-input` Remove leading/trailing whitespaces from search value
647  * @since 2.9.3
648  * @param bool $trim_search_value True: remove whitespace; False: keep as is [Default: true]
649  */
650  $trim_search_value = apply_filters( 'gravityview/search-trim-input', true );
651 
652  // add free search
653  if ( isset( $get['gv_search'] ) && '' !== $get['gv_search'] && in_array( 'search_all', $searchable_fields ) ) {
654 
655  $search_all_value = $trim_search_value ? trim( $get['gv_search'] ) : $get['gv_search'];
656 
657  if ( $split_words ) {
658  // Search for a piece
659  $words = explode( ' ', $search_all_value );
660 
661  $words = array_filter( $words );
662 
663  } else {
664  // Replace multiple spaces with one space
665  $search_all_value = preg_replace( '/\s+/ism', ' ', $search_all_value );
666 
667  $words = array( $search_all_value );
668  }
669 
670  foreach ( $words as $word ) {
671  $search_criteria['field_filters'][] = array(
672  'key' => null, // The field ID to search
673  'value' => $word, // The value to search
674  'operator' => 'contains', // What to search in. Options: `is` or `contains`
675  );
676  }
677  }
678 
679  // start date & end date
680  if ( in_array( 'entry_date', $searchable_fields ) ) {
681  /**
682  * Get and normalize the dates according to the input format.
683  */
684  if ( $curr_start = ! empty( $get['gv_start'] ) ? $get['gv_start'] : '' ) {
685  if( $curr_start_date = date_create_from_format( $this->get_datepicker_format( true ), $curr_start ) ) {
686  $curr_start = $curr_start_date->format( 'Y-m-d' );
687  }
688  }
689 
690  if ( $curr_end = ! empty( $get['gv_start'] ) ? ( ! empty( $get['gv_end'] ) ? $get['gv_end'] : '' ) : '' ) {
691  if( $curr_end_date = date_create_from_format( $this->get_datepicker_format( true ), $curr_end ) ) {
692  $curr_end = $curr_end_date->format( 'Y-m-d' );
693  }
694  }
695 
696  if ( $view ) {
697  /**
698  * Override start and end dates if View is limited to some already.
699  */
700  if ( $start_date = $view->settings->get( 'start_date' ) ) {
701  if ( $start_timestamp = strtotime( $curr_start ) ) {
702  $curr_start = $start_timestamp < strtotime( $start_date ) ? $start_date : $curr_start;
703  }
704  }
705  if ( $end_date = $view->settings->get( 'end_date' ) ) {
706  if ( $end_timestamp = strtotime( $curr_end ) ) {
707  $curr_end = $end_timestamp > strtotime( $end_date ) ? $end_date : $curr_end;
708  }
709  }
710  }
711 
712  /**
713  * @filter `gravityview_date_created_adjust_timezone` Whether to adjust the timezone for entries. \n
714  * `date_created` is stored in UTC format. Convert search date into UTC (also used on templates/fields/date_created.php). \n
715  * This is for backward compatibility before \GF_Query started to automatically apply the timezone offset.
716  * @since 1.12
717  * @param boolean $adjust_tz Use timezone-adjusted datetime? If true, adjusts date based on blog's timezone setting. If false, uses UTC setting. Default is `false`.
718  * @param string $context Where the filter is being called from. `search` in this case.
719  */
720  $adjust_tz = apply_filters( 'gravityview_date_created_adjust_timezone', false, 'search' );
721 
722  /**
723  * Don't set $search_criteria['start_date'] if start_date is empty as it may lead to bad query results (GFAPI::get_entries)
724  */
725  if ( ! empty( $curr_start ) ) {
726  $curr_start = date( 'Y-m-d H:i:s', strtotime( $curr_start ) );
727  $search_criteria['start_date'] = $adjust_tz ? get_gmt_from_date( $curr_start ) : $curr_start;
728  }
729 
730  if ( ! empty( $curr_end ) ) {
731  // Fast-forward 24 hour on the end time
732  $curr_end = date( 'Y-m-d H:i:s', strtotime( $curr_end ) + DAY_IN_SECONDS );
733  $search_criteria['end_date'] = $adjust_tz ? get_gmt_from_date( $curr_end ) : $curr_end;
734  if ( strpos( $search_criteria['end_date'], '00:00:00' ) ) { // See https://github.com/gravityview/GravityView/issues/1056
735  $search_criteria['end_date'] = date( 'Y-m-d H:i:s', strtotime( $search_criteria['end_date'] ) - 1 );
736  }
737  }
738  }
739 
740  // search for a specific entry ID
741  if ( ! empty( $get[ 'gv_id' ] ) && in_array( 'entry_id', $searchable_fields ) ) {
742  $search_criteria['field_filters'][] = array(
743  'key' => 'id',
744  'value' => absint( $get[ 'gv_id' ] ),
745  'operator' => $this->get_operator( $get, 'gv_id', array( '=' ), '=' ),
746  );
747  }
748 
749  // search for a specific Created_by ID
750  if ( ! empty( $get[ 'gv_by' ] ) && in_array( 'created_by', $searchable_fields ) ) {
751  $search_criteria['field_filters'][] = array(
752  'key' => 'created_by',
753  'value' => $get['gv_by'],
754  'operator' => $this->get_operator( $get, 'gv_by', array( '=' ), '=' ),
755  );
756  }
757 
758  // Get search mode passed in URL
759  $mode = isset( $get['mode'] ) && in_array( $get['mode'], array( 'any', 'all' ) ) ? $get['mode'] : 'any';
760 
761  // get the other search filters
762  foreach ( $get as $key => $value ) {
763  if ( 0 !== strpos( $key, 'filter_' ) && 0 !== strpos( $key, 'input_' ) ) {
764  continue;
765  }
766 
767  if ( strpos( $key, '|op' ) !== false ) {
768  continue; // This is an operator
769  }
770 
771  $filter_key = $this->convert_request_key_to_filter_key( $key );
772 
773  if ( $trim_search_value ) {
774  $value = is_array( $value ) ? array_map( 'trim', $value ) : trim( $value );
775  }
776 
777  if ( gv_empty( $value, false, false ) || ( is_array( $value ) && count( $value ) === 1 && gv_empty( $value[0], false, false ) ) ) {
778  /**
779  * @filter `gravityview/search/ignore-empty-values` Filter to control if empty field values should be ignored or strictly matched (default: true)
780  * @since 2.14.2.1
781  * @param bool $ignore_empty_values
782  * @param int|null $filter_key
783  * @param int|null $view_id
784  * @param int|null $form_id
785  */
786  $ignore_empty_values = apply_filters( 'gravityview/search/ignore-empty-values', true, $filter_key, $view_id, $form_id );
787 
788  if ( is_array( $value ) || $ignore_empty_values ) {
789  continue;
790  }
791 
792  $value = '';
793  }
794 
795  if ( $form_id && '' === $value ) {
796  $field = GFAPI::get_field( $form_id, $filter_key );
797 
798  // GF_Query casts Number field values to decimal, which may return unexpected result when the value is blank.
799  if ( $field && 'number' === $field->type ) {
800  $value = '-' . PHP_INT_MAX;
801  }
802  }
803 
804  if ( ! $filter = $this->prepare_field_filter( $filter_key, $value, $view, $searchable_field_objects, $get ) ) {
805  continue;
806  }
807 
808  if ( ! isset( $filter['operator'] ) ) {
809  $filter['operator'] = $this->get_operator( $get, $key, array( 'contains' ), 'contains' );
810  }
811 
812  if ( isset( $filter[0]['value'] ) ) {
813  $filter[0]['value'] = $trim_search_value ? trim( $filter[0]['value'] ) : $filter[0]['value'];
814 
815  $search_criteria['field_filters'] = array_merge( $search_criteria['field_filters'], $filter );
816 
817  // if date range type, set search mode to ALL
818  if ( ! empty( $filter[0]['operator'] ) && in_array( $filter[0]['operator'], array( '>=', '<=', '>', '<' ) ) ) {
819  $mode = 'all';
820  }
821  } elseif( !empty( $filter ) ) {
822  $search_criteria['field_filters'][] = $filter;
823  }
824  }
825 
826  /**
827  * @filter `gravityview/search/mode` Set the Search Mode (`all` or `any`)
828  * @since 1.5.1
829  * @param string $mode Search mode (`any` vs `all`)
830  */
831  $search_criteria['field_filters']['mode'] = apply_filters( 'gravityview/search/mode', $mode );
832 
833  gravityview()->log->debug( 'Returned Search Criteria: ', array( 'data' => $search_criteria ) );
834 
835  unset( $get );
836 
837  return $search_criteria;
838  }
839 
840  /**
841  * Filters the \GF_Query with advanced logic.
842  *
843  * Dropin for the legacy flat filters when \GF_Query is available.
844  *
845  * @param \GF_Query $query The current query object reference
846  * @param \GV\View $this The current view object
847  * @param \GV\Request $request The request object
848  */
849  public function gf_query_filter( &$query, $view, $request ) {
850  /**
851  * This is a shortcut to get all the needed search criteria.
852  * We feed these into an new GF_Query and tack them onto the current object.
853  */
854  $search_criteria = $this->filter_entries( array(), null, array( 'id' => $view->ID ), true /** force search_criteria */ );
855 
856  /**
857  * Call any userland filters that they might have.
858  */
859  remove_filter( 'gravityview_fe_search_criteria', array( $this, 'filter_entries' ), 10, 3 );
860  $search_criteria = apply_filters( 'gravityview_fe_search_criteria', $search_criteria, $view->form->ID, $view->settings->as_atts() );
861  add_filter( 'gravityview_fe_search_criteria', array( $this, 'filter_entries' ), 10, 3 );
862 
863  $query_class = $view->get_query_class();
864 
865  if ( empty( $search_criteria['field_filters'] ) ) {
866  return;
867  }
868 
869  $widgets = $view->widgets->by_id( $this->widget_id );
870  if ( $widgets->count() ) {
871  $widgets = $widgets->all();
872  $widget = $widgets[0];
873 
874  $search_fields = json_decode( $widget->configuration->get( 'search_fields' ), true );
875 
876  foreach ( (array) $search_fields as $search_field ) {
877  if ( 'created_by' === $search_field['field'] && 'input_text' === $search_field['input'] ) {
878  $created_by_text_mode = true;
879  }
880  }
881  }
882 
883  $extra_conditions = array();
884  $mode = 'any';
885 
886  foreach ( $search_criteria['field_filters'] as $key => &$filter ) {
887  if ( ! is_array( $filter ) ) {
888  if ( in_array( strtolower( $filter ), array( 'any', 'all' ) ) ) {
889  $mode = $filter;
890  }
891  continue;
892  }
893 
894  // Construct a manual query for unapproved statuses
895  if ( 'is_approved' === $filter['key'] && in_array( \GravityView_Entry_Approval_Status::UNAPPROVED, (array) $filter['value'] ) ) {
896  $_tmp_query = new $query_class( $view->form->ID, array(
897  'field_filters' => array(
898  array(
899  'operator' => 'in',
900  'key' => 'is_approved',
901  'value' => (array) $filter['value'],
902  ),
903  array(
904  'operator' => 'is',
905  'key' => 'is_approved',
906  'value' => '',
907  ),
908  'mode' => 'any'
909  ),
910  ) );
911  $_tmp_query_parts = $_tmp_query->_introspect();
912 
913  $extra_conditions[] = $_tmp_query_parts['where'];
914 
915  $filter = false;
916  continue;
917  }
918 
919  // Construct manual query for text mode creator search
920  if ( 'created_by' === $filter['key'] && ! empty( $created_by_text_mode ) ) {
921  $extra_conditions[] = new GravityView_Widget_Search_Author_GF_Query_Condition( $filter, $view );
922  $filter = false;
923  continue;
924  }
925 
926  // By default, we want searches to be wildcard for each field.
927  $filter['operator'] = empty( $filter['operator'] ) ? 'contains' : $filter['operator'];
928 
929  // For multichoice, let's have an in (OR) search.
930  if ( is_array( $filter['value'] ) ) {
931  $filter['operator'] = 'in'; // @todo what about in contains (OR LIKE chains)?
932  }
933 
934  // Default form with joins functionality
935  if ( empty( $filter['form_id'] ) ) {
936  $filter['form_id'] = $view->form ? $view->form->ID : 0;
937  }
938 
939  /**
940  * @filter `gravityview_search_operator` Modify the search operator for the field (contains, is, isnot, etc)
941  * @since 2.0 Added $view parameter
942  * @param string $operator Existing search operator
943  * @param array $filter array with `key`, `value`, `operator`, `type` keys
944  * @param \GV\View $view The View we're operating on.
945  */
946  $filter['operator'] = apply_filters( 'gravityview_search_operator', $filter['operator'], $filter, $view );
947 
948  if ( 'is' !== $filter['operator'] && '' === $filter['value'] ) {
949  unset( $search_criteria['field_filters'][ $key ] );
950  }
951  }
952 
953  if ( ! empty( $search_criteria['start_date'] ) || ! empty( $search_criteria['end_date'] ) ) {
954  $date_criteria = array();
955 
956  if ( isset( $search_criteria['start_date'] ) ) {
957  $date_criteria['start_date'] = $search_criteria['start_date'];
958  }
959 
960  if ( isset( $search_criteria['end_date'] ) ) {
961  $date_criteria['end_date'] = $search_criteria['end_date'];
962  }
963 
964  $_tmp_query = new $query_class( $view->form->ID, $date_criteria );
965  $_tmp_query_parts = $_tmp_query->_introspect();
966  $extra_conditions[] = $_tmp_query_parts['where'];
967  }
968 
969  $search_conditions = array();
970 
971  if ( $filters = array_filter( $search_criteria['field_filters'] ) ) {
972  foreach ( $filters as &$filter ) {
973  if ( ! is_array( $filter ) ) {
974  continue;
975  }
976 
977  /**
978  * Parse the filter criteria to generate the needed
979  * WHERE condition. This is a trick to not write our own generation
980  * code by reusing what's inside GF_Query already as they
981  * take care of many small things like forcing numeric, etc.
982  */
983  $_tmp_query = new $query_class( $filter['form_id'], array( 'mode' => 'any', 'field_filters' => array( $filter ) ) );
984  $_tmp_query_parts = $_tmp_query->_introspect();
985  $search_condition = $_tmp_query_parts['where'];
986 
987  if ( empty( $filter['key'] ) && $search_condition->expressions ) {
988  $search_conditions[] = $search_condition;
989  } else {
990  $left = $search_condition->left;
991 
992  // When casting a column value to a certain type (e.g., happens with the Number field), GF_Query_Column is wrapped in a GF_Query_Call class.
993  if ( $left instanceof GF_Query_Call ) {
994  try {
995  $reflectionProperty = new \ReflectionProperty( $left, '_parameters' );
996  $reflectionProperty->setAccessible( true );
997 
998  $value = $reflectionProperty->getValue( $left );
999 
1000  if ( ! empty( $value[0] ) && $value[0] instanceof GF_Query_Column ) {
1001  $left = $value[0];
1002  } else {
1003  continue;
1004  }
1005  } catch ( ReflectionException $e ) {
1006  continue;
1007  }
1008  }
1009 
1010  $alias = $query->_alias( $left->field_id, $left->source, $left->is_entry_column() ? 't' : 'm' );
1011 
1012  if ( $view->joins && $left->field_id == GF_Query_Column::META ) {
1013  foreach ( $view->joins as $_join ) {
1014  $on = $_join->join_on;
1015  $join = $_join->join;
1016 
1017  $search_conditions[] = GF_Query_Condition::_or(
1018  // Join
1019  new GF_Query_Condition(
1020  new GF_Query_Column( GF_Query_Column::META, $join->ID, $query->_alias( GF_Query_Column::META, $join->ID, 'm' ) ),
1021  $search_condition->operator,
1022  $search_condition->right
1023  ),
1024  // On
1025  new GF_Query_Condition(
1026  new GF_Query_Column( GF_Query_Column::META, $on->ID, $query->_alias( GF_Query_Column::META, $on->ID, 'm' ) ),
1027  $search_condition->operator,
1028  $search_condition->right
1029  )
1030  );
1031  }
1032  } else {
1033  $search_conditions[] = new GF_Query_Condition(
1034  new GF_Query_Column( $left->field_id, $left->source, $alias ),
1035  $search_condition->operator,
1036  $search_condition->right
1037  );
1038  }
1039  }
1040  }
1041 
1042  if ( $search_conditions ) {
1043  $search_conditions = array( call_user_func_array( '\GF_Query_Condition::' . ( $mode == 'all' ? '_and' : '_or' ), $search_conditions ) );
1044  }
1045  }
1046 
1047  /**
1048  * Grab the current clauses. We'll be combining them shortly.
1049  */
1050  $query_parts = $query->_introspect();
1051 
1052  /**
1053  * Combine the parts as a new WHERE clause.
1054  */
1055  $where = call_user_func_array( '\GF_Query_Condition::_and', array_merge( array( $query_parts['where'] ), $search_conditions, $extra_conditions ) );
1056  $query->where( $where );
1057  }
1058 
1059  /**
1060  * Convert $_GET/$_POST key to the field/meta ID
1061  *
1062  * Examples:
1063  * - `filter_is_starred` => `is_starred`
1064  * - `filter_1_2` => `1.2`
1065  * - `filter_5` => `5`
1066  *
1067  * @since 2.0
1068  *
1069  * @param string $key $_GET/_$_POST search key
1070  *
1071  * @return string
1072  */
1073  private function convert_request_key_to_filter_key( $key ) {
1074 
1075  $field_id = str_replace( array( 'filter_', 'input_' ), '', $key );
1076 
1077  // calculates field_id, removing 'filter_' and for '_' for advanced fields ( like name or checkbox )
1078  if ( preg_match('/^[0-9_]+$/ism', $field_id ) ) {
1079  $field_id = str_replace( '_', '.', $field_id );
1080  }
1081 
1082  return $field_id;
1083  }
1084 
1085  /**
1086  * Prepare the field filters to GFAPI
1087  *
1088  * 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.
1089  *
1090  * Format searched values
1091  *
1092  * @param string $filter_key ID of the field, or entry meta key
1093  * @param string $value $_GET/$_POST search value
1094  * @param \GV\View $view The view we're looking at
1095  * @param array[] $searchable_fields The searchable fields as configured by the widget.
1096  * @param string[] $get The $_GET/$_POST array.
1097  *
1098  * @since develop Added 5th $get parameter for operator overrides.
1099  * @todo Set function as private.
1100  *
1101  * @return array|false 1 or 2 deph levels, false if not allowed
1102  */
1103  public function prepare_field_filter( $filter_key, $value, $view, $searchable_fields, $get = array() ) {
1104  $key = $filter_key;
1105  $filter_key = explode( ':', $filter_key ); // field_id, form_id
1106 
1107  $form = null;
1108 
1109  if ( count( $filter_key ) > 1 ) {
1110  // form is specified
1111  list( $field_id, $form_id ) = $filter_key;
1112 
1113  if ( $forms = \GV\View::get_joined_forms( $view->ID ) ) {
1114  if ( ! $form = \GV\GF_Form::by_id( $form_id ) ) {
1115  return false;
1116  }
1117  }
1118 
1119  // form is allowed
1120  $found = false;
1121  foreach ( $forms as $form ) {
1122  if ( $form->ID == $form_id ) {
1123  $found = true;
1124  break;
1125  }
1126  }
1127 
1128  if ( ! $found ) {
1129  return false;
1130  }
1131 
1132  // form is in searchable fields
1133  $found = false;
1134  foreach ( $searchable_fields as $field ) {
1135  if ( $field_id == $field['field'] && $form->ID == $field['form_id'] ) {
1136  $found = true;
1137  break;
1138  }
1139  }
1140 
1141  if ( ! $found ) {
1142  return false;
1143  }
1144  } else {
1145  $field_id = reset( $filter_key );
1146  $searchable_fields = wp_list_pluck( $searchable_fields, 'field' );
1147  if ( ! in_array( 'search_all', $searchable_fields ) && ! in_array( $field_id, $searchable_fields ) ) {
1148  return false;
1149  }
1150  }
1151 
1152  if ( ! $form ) {
1153  // fallback
1154  $form = $view->form;
1155  }
1156 
1157  // get form field array
1158  $form_field = is_numeric( $field_id ) ? \GV\GF_Field::by_id( $form, $field_id ) : \GV\Internal_Field::by_id( $field_id );
1159 
1160  if ( ! $form_field ) {
1161  return false;
1162  }
1163 
1164  // default filter array
1165  $filter = array(
1166  'key' => $field_id,
1167  'value' => $value,
1168  'form_id' => $form->ID,
1169  );
1170 
1171  switch ( $form_field->type ) {
1172 
1173  case 'select':
1174  case 'radio':
1175  $filter['operator'] = $this->get_operator( $get, $key, array( 'is' ), 'is' );
1176  break;
1177 
1178  case 'post_category':
1179 
1180  if ( ! is_array( $value ) ) {
1181  $value = array( $value );
1182  }
1183 
1184  // Reset filter variable
1185  $filter = array();
1186 
1187  foreach ( $value as $val ) {
1188  $cat = get_term( $val, 'category' );
1189  $filter[] = array(
1190  'key' => $field_id,
1191  'value' => esc_attr( $cat->name ) . ':' . $val,
1192  'operator' => $this->get_operator( $get, $key, array( 'is' ), 'is' ),
1193  );
1194  }
1195 
1196  break;
1197 
1198  case 'multiselect':
1199 
1200  if ( ! is_array( $value ) ) {
1201  break;
1202  }
1203 
1204  // Reset filter variable
1205  $filter = array();
1206 
1207  foreach ( $value as $val ) {
1208  $filter[] = array( 'key' => $field_id, 'value' => $val );
1209  }
1210 
1211  break;
1212 
1213  case 'checkbox':
1214  // convert checkbox on/off into the correct search filter
1215  if ( false !== strpos( $field_id, '.' ) && ! empty( $form_field->inputs ) && ! empty( $form_field->choices ) ) {
1216  foreach ( $form_field->inputs as $k => $input ) {
1217  if ( $input['id'] == $field_id ) {
1218  $filter['value'] = $form_field->choices[ $k ]['value'];
1219  $filter['operator'] = $this->get_operator( $get, $key, array( 'is' ), 'is' );
1220  break;
1221  }
1222  }
1223  } elseif ( is_array( $value ) ) {
1224 
1225  // Reset filter variable
1226  $filter = array();
1227 
1228  foreach ( $value as $val ) {
1229  $filter[] = array(
1230  'key' => $field_id,
1231  'value' => $val,
1232  'operator' => $this->get_operator( $get, $key, array( 'is' ), 'is' ),
1233  );
1234  }
1235  }
1236 
1237  break;
1238 
1239  case 'name':
1240  case 'address':
1241 
1242  if ( false === strpos( $field_id, '.' ) ) {
1243 
1244  $words = explode( ' ', $value );
1245 
1246  $filters = array();
1247  foreach ( $words as $word ) {
1248  if ( ! empty( $word ) && strlen( $word ) > 1 ) {
1249  // Keep the same key for each filter
1250  $filter['value'] = $word;
1251  // Add a search for the value
1252  $filters[] = $filter;
1253  }
1254  }
1255 
1256  $filter = $filters;
1257  }
1258 
1259  // State/Province should be exact matches
1260  if ( 'address' === $form_field->field->type ) {
1261 
1262  $searchable_fields = $this->get_view_searchable_fields( $view, true );
1263 
1264  foreach ( $searchable_fields as $searchable_field ) {
1265 
1266  if( $form_field->ID !== $searchable_field['field'] ) {
1267  continue;
1268  }
1269 
1270  // Only exact-match dropdowns, not text search
1271  if( in_array( $searchable_field['input'], array( 'text', 'search' ), true ) ) {
1272  continue;
1273  }
1274 
1275  $input_id = gravityview_get_input_id_from_id( $form_field->ID );
1276 
1277  if ( 4 === $input_id ) {
1278  $filter['operator'] = $this->get_operator( $get, $key, array( 'is' ), 'is' );
1279  };
1280  }
1281  }
1282 
1283  break;
1284 
1285  case 'payment_date':
1286  case 'date':
1287 
1288  $date_format = $this->get_datepicker_format( true );
1289 
1290  if ( is_array( $value ) ) {
1291 
1292  // Reset filter variable
1293  $filter = array();
1294 
1295  foreach ( $value as $k => $date ) {
1296  if ( empty( $date ) ) {
1297  continue;
1298  }
1299  $operator = 'start' === $k ? '>=' : '<=';
1300 
1301  /**
1302  * @hack
1303  * @since 1.16.3
1304  * Safeguard until GF implements '<=' operator
1305  */
1306  if( !GFFormsModel::is_valid_operator( $operator ) && $operator === '<=' ) {
1307  $operator = '<';
1308  $date = date( 'Y-m-d', strtotime( self::get_formatted_date( $date, 'Y-m-d', $date_format ) . ' +1 day' ) );
1309  }
1310 
1311  $filter[] = array(
1312  'key' => $field_id,
1313  'value' => self::get_formatted_date( $date, 'Y-m-d', $date_format ),
1314  'operator' => $this->get_operator( $get, $key, array( $operator ), $operator ),
1315  );
1316  }
1317  } else {
1318  $date = $value;
1319  $filter['value'] = self::get_formatted_date( $date, 'Y-m-d', $date_format );
1320  $filter['operator'] = $this->get_operator( $get, $key, array( 'is' ), 'is' );
1321  }
1322 
1323  if ('payment_date' === $key) {
1324  $filter['operator'] = 'contains';
1325  }
1326 
1327  break;
1328  } // switch field type
1329 
1330  return $filter;
1331  }
1332 
1333  /**
1334  * Get the Field Format form GravityForms
1335  *
1336  * @param GF_Field_Date $field The field object
1337  * @since 1.10
1338  *
1339  * @return string Format of the date in the database
1340  */
1341  public static function get_date_field_format( GF_Field_Date $field ) {
1342  $format = 'm/d/Y';
1343  $datepicker = array(
1344  'mdy' => 'm/d/Y',
1345  'dmy' => 'd/m/Y',
1346  'dmy_dash' => 'd-m-Y',
1347  'dmy_dot' => 'd.m.Y',
1348  'ymd_slash' => 'Y/m/d',
1349  'ymd_dash' => 'Y-m-d',
1350  'ymd_dot' => 'Y.m.d',
1351  );
1352 
1353  if ( ! empty( $field->dateFormat ) && isset( $datepicker[ $field->dateFormat ] ) ){
1354  $format = $datepicker[ $field->dateFormat ];
1355  }
1356 
1357  return $format;
1358  }
1359 
1360  /**
1361  * Format a date value
1362  *
1363  * @param string $value Date value input
1364  * @param string $format Wanted formatted date
1365  *
1366  * @since 2.1.2
1367  * @param string $value_format The value format. Default: Y-m-d
1368  *
1369  * @return string
1370  */
1371  public static function get_formatted_date( $value = '', $format = 'Y-m-d', $value_format = 'Y-m-d' ) {
1372 
1373  $date = date_create_from_format( $value_format, $value );
1374 
1375  if ( empty( $date ) ) {
1376  gravityview()->log->debug( 'Date format not valid: {value}', array( 'value' => $value ) );
1377  return '';
1378  }
1379  return $date->format( $format );
1380  }
1381 
1382 
1383  /**
1384  * Include this extension templates path
1385  * @param array $file_paths List of template paths ordered
1386  */
1387  public function add_template_path( $file_paths ) {
1388 
1389  // Index 100 is the default GravityView template path.
1390  $file_paths[102] = self::$file . 'templates/';
1391 
1392  return $file_paths;
1393  }
1394 
1395  /**
1396  * Check whether the configured search fields have a date field
1397  *
1398  * @since 1.17.5
1399  *
1400  * @param array $search_fields
1401  *
1402  * @return bool True: has a `date` or `date_range` field
1403  */
1404  private function has_date_field( $search_fields ) {
1405 
1406  $has_date = false;
1407 
1408  foreach ( $search_fields as $k => $field ) {
1409  if ( in_array( $field['input'], array( 'date', 'date_range', 'entry_date' ) ) ) {
1410  $has_date = true;
1411  break;
1412  }
1413  }
1414 
1415  return $has_date;
1416  }
1417 
1418  /**
1419  * Renders the Search Widget
1420  * @param array $widget_args
1421  * @param string $content
1422  * @param string|\GV\Template_Context $context
1423  *
1424  * @return void
1425  */
1426  public function render_frontend( $widget_args, $content = '', $context = '' ) {
1427 
1429 
1430  if ( empty( $gravityview_view ) ) {
1431  gravityview()->log->debug( '$gravityview_view not instantiated yet.' );
1432  return;
1433  }
1434 
1435  $view = \GV\View::by_id( $gravityview_view->view_id );
1436 
1437  // get configured search fields
1438  $search_fields = ! empty( $widget_args['search_fields'] ) ? json_decode( $widget_args['search_fields'], true ) : '';
1439 
1440  if ( empty( $search_fields ) || ! is_array( $search_fields ) ) {
1441  gravityview()->log->debug( 'No search fields configured for widget:', array( 'data' => $widget_args ) );
1442  return;
1443  }
1444 
1445  // prepare fields
1446  foreach ( $search_fields as $k => $field ) {
1447 
1448  $updated_field = $field;
1449 
1450  $updated_field = $this->get_search_filter_details( $updated_field, $context, $widget_args );
1451 
1452  switch ( $field['field'] ) {
1453 
1454  case 'search_all':
1455  $updated_field['key'] = 'search_all';
1456  $updated_field['input'] = 'search_all';
1457  $updated_field['value'] = $this->rgget_or_rgpost( 'gv_search' );
1458  break;
1459 
1460  case 'entry_date':
1461  $updated_field['key'] = 'entry_date';
1462  $updated_field['input'] = 'entry_date';
1463  $updated_field['value'] = array(
1464  'start' => $this->rgget_or_rgpost( 'gv_start' ),
1465  'end' => $this->rgget_or_rgpost( 'gv_end' ),
1466  );
1467  break;
1468 
1469  case 'entry_id':
1470  $updated_field['key'] = 'entry_id';
1471  $updated_field['input'] = 'entry_id';
1472  $updated_field['value'] = $this->rgget_or_rgpost( 'gv_id' );
1473  break;
1474 
1475  case 'created_by':
1476  $updated_field['key'] = 'created_by';
1477  $updated_field['name'] = 'gv_by';
1478  $updated_field['value'] = $this->rgget_or_rgpost( 'gv_by' );
1479  break;
1480 
1481  case 'is_approved':
1482  $updated_field['key'] = 'is_approved';
1483  $updated_field['value'] = $this->rgget_or_rgpost( 'filter_is_approved' );
1484  $updated_field['choices'] = self::get_is_approved_choices();
1485  break;
1486  }
1487 
1488  $search_fields[ $k ] = $updated_field;
1489  }
1490 
1491  gravityview()->log->debug( 'Calculated Search Fields: ', array( 'data' => $search_fields ) );
1492 
1493  /**
1494  * @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.
1495  * @param array $search_fields Array of search filters with `key`, `label`, `value`, `type`, `choices` keys
1496  * @param GravityView_Widget_Search $this Current widget object
1497  * @param array $widget_args Args passed to this method. {@since 1.8}
1498  * @param \GV\Template_Context $context {@since 2.0}
1499  * @type array
1500  */
1501  $gravityview_view->search_fields = apply_filters( 'gravityview_widget_search_filters', $search_fields, $this, $widget_args, $context );
1502 
1503  $gravityview_view->permalink_fields = $this->add_no_permalink_fields( array(), $this, $widget_args );
1504 
1505  $gravityview_view->search_layout = ! empty( $widget_args['search_layout'] ) ? $widget_args['search_layout'] : 'horizontal';
1506 
1507  /** @since 1.14 */
1508  $gravityview_view->search_mode = ! empty( $widget_args['search_mode'] ) ? $widget_args['search_mode'] : 'any';
1509 
1510  $custom_class = ! empty( $widget_args['custom_class'] ) ? $widget_args['custom_class'] : '';
1511 
1512  $gravityview_view->search_class = self::get_search_class( $custom_class );
1513 
1514  $gravityview_view->search_clear = ! empty( $widget_args['search_clear'] ) ? $widget_args['search_clear'] : false;
1515 
1516  if ( $this->has_date_field( $search_fields ) ) {
1517  // enqueue datepicker stuff only if needed!
1518  $this->enqueue_datepicker();
1519  }
1520 
1521  $this->maybe_enqueue_flexibility();
1522 
1523  $gravityview_view->render( 'widget', 'search', false );
1524  }
1525 
1526  /**
1527  * Get the search class for a search form
1528  *
1529  * @since 1.5.4
1530  *
1531  * @return string Sanitized CSS class for the search form
1532  */
1533  public static function get_search_class( $custom_class = '' ) {
1535 
1536  $search_class = 'gv-search-'.$gravityview_view->search_layout;
1537 
1538  if ( ! empty( $custom_class ) ) {
1539  $search_class .= ' '.$custom_class;
1540  }
1541 
1542  /**
1543  * @filter `gravityview_search_class` Modify the CSS class for the search form
1544  * @param string $search_class The CSS class for the search form
1545  */
1546  $search_class = apply_filters( 'gravityview_search_class', $search_class );
1547 
1548  // Is there an active search being performed? Used by fe-views.js
1549  $search_class .= gravityview()->request->is_search() || GravityView_frontend::getInstance()->isSearch() ? ' gv-is-search' : '';
1550 
1551  return gravityview_sanitize_html_class( $search_class );
1552  }
1553 
1554 
1555  /**
1556  * Calculate the search form action
1557  * @since 1.6
1558  *
1559  * @return string
1560  */
1561  public static function get_search_form_action() {
1563 
1564  $post_id = $gravityview_view->getPostId() ? $gravityview_view->getPostId() : $gravityview_view->getViewId();
1565 
1566  $url = add_query_arg( array(), get_permalink( $post_id ) );
1567 
1568  /**
1569  * @filter `gravityview/widget/search/form/action` Override the search URL.
1570  * @param string $action Where the form submits to.
1571  *
1572  * Further parameters will be added once adhoc context is added.
1573  * Use gravityview()->request until then.
1574  */
1575  return apply_filters( 'gravityview/widget/search/form/action', $url );
1576  }
1577 
1578  /**
1579  * Get the label for a search form field
1580  * @param array $field Field setting as sent by the GV configuration - has `field`, `input` (input type), and `label` keys
1581  * @param array $form_field Form field data, as fetched by `gravityview_get_field()`
1582  * @return string Label for the search form
1583  */
1584  private static function get_field_label( $field, $form_field = array() ) {
1585 
1586  $label = \GV\Utils::_GET( 'label', \GV\Utils::get( $field, 'label' ) );
1587 
1588  if ( ! $label ) {
1589 
1590  $label = isset( $form_field['label'] ) ? $form_field['label'] : '';
1591 
1592  switch( $field['field'] ) {
1593  case 'search_all':
1594  $label = __( 'Search Entries:', 'gk-gravityview' );
1595  break;
1596  case 'entry_date':
1597  $label = __( 'Filter by date:', 'gk-gravityview' );
1598  break;
1599  case 'entry_id':
1600  $label = __( 'Entry ID:', 'gk-gravityview' );
1601  break;
1602  default:
1603  // If this is a field input, not a field
1604  if ( strpos( $field['field'], '.' ) > 0 && ! empty( $form_field['inputs'] ) ) {
1605 
1606  // Get the label for the field in question, which returns an array
1607  $items = wp_list_filter( $form_field['inputs'], array( 'id' => $field['field'] ) );
1608 
1609  // Get the item with the `label` key
1610  $values = wp_list_pluck( $items, 'label' );
1611 
1612  // There will only one item in the array, but this is easier
1613  foreach ( $values as $value ) {
1614  $label = $value;
1615  break;
1616  }
1617  }
1618  }
1619  }
1620 
1621  /**
1622  * @filter `gravityview_search_field_label` Modify the label for a search field. Supports returning HTML
1623  * @since 1.17.3 Added $field parameter
1624  * @param string $label Existing label text, sanitized.
1625  * @param array $form_field Gravity Forms field array, as returned by `GFFormsModel::get_field()`
1626  * @param array $field Field setting as sent by the GV configuration - has `field`, `input` (input type), and `label` keys
1627  */
1628  $label = apply_filters( 'gravityview_search_field_label', esc_attr( $label ), $form_field, $field );
1629 
1630  return $label;
1631  }
1632 
1633  /**
1634  * Prepare search fields to frontend render with other details (label, field type, searched values)
1635  *
1636  * @since 2.16 Added $widget_args parameter.
1637  *
1638  * @param array $field
1639  * @param \GV\Context $context
1640  * @param array $widget_args
1641  *
1642  * @return array
1643  */
1644  private function get_search_filter_details( $field, $context, $widget_args ) {
1645 
1647 
1648  $form = $gravityview_view->getForm();
1649 
1650  // for advanced field ids (eg, first name / last name )
1651  $name = 'filter_' . str_replace( '.', '_', $field['field'] );
1652 
1653  // get searched value from $_GET/$_POST (string or array)
1654  $value = $this->rgget_or_rgpost( $name );
1655 
1656  // get form field details
1657  $form_field = gravityview_get_field( $form, $field['field'] );
1658 
1659  $form_field_type = \GV\Utils::get( $form_field, 'type' );
1660 
1661  $filter = array(
1662  'key' => \GV\Utils::get( $field, 'field' ),
1663  'name' => $name,
1664  'label' => self::get_field_label( $field, $form_field ),
1665  'input' => \GV\Utils::get( $field, 'input' ),
1666  'value' => $value,
1667  'type' => $form_field_type,
1668  );
1669 
1670  // collect choices
1671  if ( 'post_category' === $form_field_type && ! empty( $form_field['displayAllCategories'] ) && empty( $form_field['choices'] ) ) {
1672  $filter['choices'] = gravityview_get_terms_choices();
1673  } elseif ( ! empty( $form_field['choices'] ) ) {
1674  $filter['choices'] = $form_field['choices'];
1675  }
1676 
1677  if ( 'date_range' === $field['input'] && empty( $value ) ) {
1678  $filter['value'] = array( 'start' => '', 'end' => '' );
1679  }
1680 
1681  if ( 'created_by' === $field['field'] ) {
1682  $filter['choices'] = self::get_created_by_choices( ( isset( $context->view ) ? $context->view : null ) );
1683  $filter['type'] = 'created_by';
1684  }
1685 
1686  /**
1687  * @filter `gravityview/search/filter_details` Filter the output filter details for the Search widget.
1688  * @since 2.5
1689  * @param array $filter The filter details
1690  * @param array $field The search field configuration
1691  * @param \GV\Context The context
1692  */
1693  return apply_filters( 'gravityview/search/filter_details', $filter, $field, $context );
1694  }
1695 
1696  /**
1697  * If sieve choices is enabled, run it for each of the fields with choices.
1698  *
1699  * @since 2.16.6
1700  *
1701  * @uses sieve_filter_choices
1702  *
1703  * @param array $search_fields Array of search filters with `key`, `label`, `value`, `type` keys
1704  * @param GravityView_Widget_Search $widget Current widget object
1705  * @param array $widget_args Args passed to this method. {@since 1.8}
1706  * @param \GV\Template_Context $context
1707  *
1708  * @return array If the search field GF Field type is `address`, and there are choices to add, adds them and changes the input type. Otherwise, sets the input to text.
1709  */
1710  public function maybe_sieve_filter_choices( $search_fields, $widget, $widget_args, $context ) {
1711 
1712  $sieve_choices = \GV\Utils::get( $widget_args, 'sieve_choices', false );
1713 
1714  if ( ! $sieve_choices ) {
1715  return $search_fields;
1716  }
1717 
1718  foreach ( $search_fields as &$filter ) {
1719  if ( empty( $filter['choices'] ) ) {
1720  continue;
1721  }
1722 
1723  $field = gravityview_get_field( $context->view->form->form, $filter['key'] ); // @todo Support multiple forms (joins)
1724 
1725  /**
1726  * @filter `gravityview/search/sieve_choices` Only output used choices for this field.
1727  * @since 2.16 Modified default value to the `sieve_choices` widget setting and added $widget_args parameter.
1728  *
1729  * @param bool $sieve_choices True: Yes, filter choices based on whether the value exists in entries. False: show all choices in the original field. Default: false.
1730  * @param array $field The field configuration.
1731  * @param \GV\Context The context.
1732  */
1733  if ( apply_filters( 'gravityview/search/sieve_choices', $sieve_choices, $field, $context, $widget_args ) ) {
1734  $filter['choices'] = $this->sieve_filter_choices( $filter, $context );
1735  }
1736  }
1737 
1738  return $search_fields;
1739  }
1740 
1741  /**
1742  * Sieve filter choices to only ones that are used.
1743  *
1744  * @param array $filter The filter configuration.
1745  * @param \GV\Context $context The context
1746  *
1747  * @since 2.5
1748  * @internal
1749  *
1750  * @return array The filter choices.
1751  */
1752  private function sieve_filter_choices( $filter, $context ) {
1753  if ( empty( $filter['key'] ) || empty( $filter['choices'] ) ) {
1754  return $filter; // @todo Populate plugins might give us empty choices
1755  }
1756 
1757  // Allow only created_by and field-ids to be sieved.
1758  if ( 'created_by' !== $filter['key'] && ! is_numeric( $filter['key'] ) ) {
1759  return $filter;
1760  }
1761 
1762  $form_id = $context->view->form->ID; // @todo Support multiple forms (joins)
1763 
1764  $cache = new GravityView_Cache( $form_id, [ 'sieve', $filter['key'], $context->view->ID ] );
1765 
1766  $filter_choices = $cache->get();
1767 
1768  if ( $filter_choices ) {
1769  return $filter_choices;
1770  }
1771 
1772  global $wpdb;
1773 
1774  $entry_table_name = GFFormsModel::get_entry_table_name();
1775  $entry_meta_table_name = GFFormsModel::get_entry_meta_table_name();
1776 
1777  $key_like = $wpdb->esc_like( $filter['key'] ) . '.%';
1778 
1779  switch ( \GV\Utils::get( $filter, 'type' ) ) {
1780  case 'post_category':
1781  $choices = $wpdb->get_col( $wpdb->prepare(
1782  "SELECT DISTINCT SUBSTRING_INDEX( `meta_value`, ':', 1) FROM $entry_meta_table_name WHERE ( `meta_key` LIKE %s OR `meta_key` = %d) AND `form_id` = %d",
1783  $key_like, $filter['key'], $form_id
1784  ) );
1785  break;
1786  case 'created_by':
1787  $choices = $wpdb->get_col( $wpdb->prepare(
1788  "SELECT DISTINCT `created_by` FROM $entry_table_name WHERE `form_id` = %d",
1789  $form_id
1790  ) );
1791  break;
1792  default:
1793  $sql = $wpdb->prepare(
1794  "SELECT DISTINCT `meta_value` FROM $entry_meta_table_name WHERE ( `meta_key` LIKE %s OR `meta_key` = %s ) AND `form_id` = %d",
1795  $key_like, $filter['key'], $form_id
1796  );
1797 
1798  $choices = $wpdb->get_col( $sql );
1799 
1800  $field = gravityview_get_field( $context->view->form->form, $filter['key'] );
1801 
1802  if ( $field && 'json' === $field->storageType ) {
1803  $choices = array_map( 'json_decode', $choices );
1804  $_choices_array = array();
1805  foreach ( $choices as $choice ) {
1806  if ( is_array( $choice ) ) {
1807  $_choices_array = array_merge( $_choices_array, $choice );
1808  } else {
1809  $_choices_array [] = $choice;
1810  }
1811  }
1812  $choices = array_unique( $_choices_array );
1813  }
1814 
1815  break;
1816  }
1817 
1818  $filter_choices = array();
1819  foreach ( $filter['choices'] as $choice ) {
1820  if ( in_array( $choice['text'], $choices, true ) || in_array( $choice['value'], $choices, true ) ) {
1821  $filter_choices[] = $choice;
1822  }
1823  }
1824 
1825  $cache->set( $filter_choices, 'sieve_filter_choices', WEEK_IN_SECONDS );
1826 
1827  return $filter_choices;
1828  }
1829 
1830  /**
1831  * Calculate the search choices for the users
1832  *
1833  * @param \GV\View|null $view The View, if set.
1834  * @since develop
1835  *
1836  * @since 1.8
1837  *
1838  * @return array Array of user choices (value = ID, text = display name)
1839  */
1840  private static function get_created_by_choices( $view ) {
1841 
1842  /**
1843  * filter gravityview/get_users/search_widget
1844  * @see \GVCommon::get_users
1845  */
1846  $users = GVCommon::get_users( 'search_widget', array( 'fields' => array( 'ID', 'display_name' ) ) );
1847 
1848  $choices = array();
1849  foreach ( $users as $user ) {
1850  /**
1851  * @filter `gravityview/search/created_by/text` Filter the display text in created by search choices
1852  * @since develop
1853  * @param string[in,out] The text. Default: $user->display_name
1854  * @param \WP_User $user The user.
1855  * @param \GV\View|null $view The view.
1856  */
1857  $text = apply_filters( 'gravityview/search/created_by/text', $user->display_name, $user, $view );
1858  $choices[] = array(
1859  'value' => $user->ID,
1860  'text' => $text,
1861  );
1862  }
1863 
1864  return $choices;
1865  }
1866 
1867  /**
1868  * Calculate the search checkbox choices for approval status
1869  *
1870  * @since develop
1871  *
1872  * @return array Array of approval status choices (value = status, text = display name)
1873  */
1874  private static function get_is_approved_choices() {
1875 
1876  $choices = array();
1877  foreach ( GravityView_Entry_Approval_Status::get_all() as $status ) {
1878  $choices[] = array(
1879  'value' => $status['value'],
1880  'text' => $status['label'],
1881  );
1882  }
1883 
1884  return $choices;
1885  }
1886 
1887  /**
1888  * Output the Clear Search Results button
1889  * @since 1.5.4
1890  */
1891  public static function the_clear_search_button() {
1893 
1894  if ( $gravityview_view->search_clear ) {
1895 
1896  $url = strtok( add_query_arg( array() ), '?' );
1897 
1898  echo gravityview_get_link( $url, esc_html__( 'Clear', 'gk-gravityview' ), 'class=button gv-search-clear' );
1899 
1900  }
1901  }
1902 
1903  /**
1904  * Based on the search method, fetch the value for a specific key
1905  *
1906  * @since 1.16.4
1907  *
1908  * @param string $name Name of the request key to fetch the value for
1909  *
1910  * @return mixed|string Value of request at $name key. Empty string if empty.
1911  */
1912  private function rgget_or_rgpost( $name ) {
1914 
1915  $value = stripslashes_deep( $value );
1916 
1917  if ( ! is_null( $value ) ) {
1918  $value = gv_map_deep( $value, 'rawurldecode' );
1919  }
1920 
1921  $value = gv_map_deep( $value, '_wp_specialchars' );
1922 
1923  return $value;
1924  }
1925 
1926 
1927  /**
1928  * Require the datepicker script for the frontend GV script
1929  * @param array $js_dependencies Array of existing required scripts for the fe-views.js script
1930  * @return array Array required scripts, with `jquery-ui-datepicker` added
1931  */
1932  public function add_datepicker_js_dependency( $js_dependencies ) {
1933 
1934  $js_dependencies[] = 'jquery-ui-datepicker';
1935 
1936  return $js_dependencies;
1937  }
1938 
1939  /**
1940  * Modify the array passed to wp_localize_script()
1941  *
1942  * @param array $js_localization The data padded to the Javascript file
1943  * @param array $view_data View data array with View settings
1944  *
1945  * @return array
1946  */
1947  public function add_datepicker_localization( $localizations = array(), $view_data = array() ) {
1948  global $wp_locale;
1949 
1950  /**
1951  * @filter `gravityview_datepicker_settings` Modify the datepicker settings
1952  * @see http://api.jqueryui.com/datepicker/ Learn what settings are available
1953  * @see http://www.renegadetechconsulting.com/tutorials/jquery-datepicker-and-wordpress-i18n Thanks for the helpful information on $wp_locale
1954  * @param array $js_localization The data padded to the Javascript file
1955  * @param array $view_data View data array with View settings
1956  */
1957  $datepicker_settings = apply_filters( 'gravityview_datepicker_settings', array(
1958  'yearRange' => '-5:+5',
1959  'changeMonth' => true,
1960  'changeYear' => true,
1961  'closeText' => esc_attr_x( 'Close', 'Close calendar', 'gk-gravityview' ),
1962  'prevText' => esc_attr_x( 'Prev', 'Previous month in calendar', 'gk-gravityview' ),
1963  'nextText' => esc_attr_x( 'Next', 'Next month in calendar', 'gk-gravityview' ),
1964  'currentText' => esc_attr_x( 'Today', 'Today in calendar', 'gk-gravityview' ),
1965  'weekHeader' => esc_attr_x( 'Week', 'Week in calendar', 'gk-gravityview' ),
1966  'monthStatus' => __( 'Show a different month', 'gk-gravityview' ),
1967  'monthNames' => array_values( $wp_locale->month ),
1968  'monthNamesShort' => array_values( $wp_locale->month_abbrev ),
1969  'dayNames' => array_values( $wp_locale->weekday ),
1970  'dayNamesShort' => array_values( $wp_locale->weekday_abbrev ),
1971  'dayNamesMin' => array_values( $wp_locale->weekday_initial ),
1972  // get the start of week from WP general setting
1973  'firstDay' => get_option( 'start_of_week' ),
1974  // is Right to left language? default is false
1975  'isRTL' => is_rtl(),
1976  ), $view_data );
1977 
1978  $localizations['datepicker'] = $datepicker_settings;
1979 
1980  return $localizations;
1981 
1982  }
1983 
1984  /**
1985  * Register search widget scripts, including Flexibility
1986  *
1987  * @see https://github.com/10up/flexibility
1988  *
1989  * @since 1.17
1990  *
1991  * @return void
1992  */
1993  public function register_scripts() {
1994  wp_register_script( 'gv-flexibility', plugins_url( 'assets/lib/flexibility/flexibility.js', GRAVITYVIEW_FILE ), array(), \GV\Plugin::$version, true );
1995  }
1996 
1997  /**
1998  * If the current visitor is running IE 8 or 9, enqueue Flexibility
1999  *
2000  * @since 1.17
2001  *
2002  * @return void
2003  */
2004  private function maybe_enqueue_flexibility() {
2005  if ( isset( $_SERVER['HTTP_USER_AGENT'] ) && preg_match( '/MSIE [8-9]/', $_SERVER['HTTP_USER_AGENT'] ) ) {
2006  wp_enqueue_script( 'gv-flexibility' );
2007  }
2008  }
2009 
2010  /**
2011  * Enqueue the datepicker script
2012  *
2013  * It sets the $gravityview->datepicker_class parameter
2014  *
2015  * @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.
2016  * @return void
2017  */
2018  public function enqueue_datepicker() {
2020 
2021  wp_enqueue_script( 'jquery-ui-datepicker' );
2022 
2023  add_filter( 'gravityview_js_dependencies', array( $this, 'add_datepicker_js_dependency' ) );
2024  add_filter( 'gravityview_js_localization', array( $this, 'add_datepicker_localization' ), 10, 2 );
2025 
2026  $scheme = is_ssl() ? 'https://' : 'http://';
2027  wp_enqueue_style( 'jquery-ui-datepicker', $scheme.'ajax.googleapis.com/ajax/libs/jqueryui/1.8.18/themes/smoothness/jquery-ui.css' );
2028 
2029  /**
2030  * @filter `gravityview_search_datepicker_class`
2031  * 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.
2032  * @param string $css_class CSS class to use. Default: `gv-datepicker datepicker mdy` \n
2033  * Options are:
2034  * - `mdy` (mm/dd/yyyy)
2035  * - `dmy` (dd/mm/yyyy)
2036  * - `dmy_dash` (dd-mm-yyyy)
2037  * - `dmy_dot` (dd.mm.yyyy)
2038  * - `ymd_slash` (yyyy/mm/dd)
2039  * - `ymd_dash` (yyyy-mm-dd)
2040  * - `ymd_dot` (yyyy.mm.dd)
2041  */
2042  $datepicker_class = apply_filters( 'gravityview_search_datepicker_class', "gv-datepicker datepicker " . $this->get_datepicker_format() );
2043 
2044  $gravityview_view->datepicker_class = $datepicker_class;
2045  }
2046 
2047  /**
2048  * Retrieve the datepicker format.
2049  *
2050  * @param bool $date_format Whether to return the PHP date format or the datpicker class name. Default: false.
2051  *
2052  * @see https://docs.gravityview.co/article/115-changing-the-format-of-the-search-widgets-date-picker
2053  *
2054  * @return string The datepicker format placeholder, or the PHP date format.
2055  */
2056  private function get_datepicker_format( $date_format = false ) {
2057 
2058  $default_format = 'mdy';
2059 
2060  /**
2061  * @filter `gravityview/widgets/search/datepicker/format`
2062  * @since 2.1.1
2063  * @param string $format Default: mdy
2064  * Options are:
2065  * - `mdy` (mm/dd/yyyy)
2066  * - `dmy` (dd/mm/yyyy)
2067  * - `dmy_dash` (dd-mm-yyyy)
2068  * - `dmy_dot` (dd.mm.yyyy)
2069  * - `ymd_slash` (yyyy/mm/dd)
2070  * - `ymd_dash` (yyyy-mm-dd)
2071  * - `ymd_dot` (yyyy.mm.dd)
2072  */
2073  $format = apply_filters( 'gravityview/widgets/search/datepicker/format', $default_format );
2074 
2075  $gf_date_formats = array(
2076  'mdy' => 'm/d/Y',
2077 
2078  'dmy_dash' => 'd-m-Y',
2079  'dmy_dot' => 'd.m.Y',
2080  'dmy' => 'd/m/Y',
2081 
2082  'ymd_slash' => 'Y/m/d',
2083  'ymd_dash' => 'Y-m-d',
2084  'ymd_dot' => 'Y.m.d',
2085  );
2086 
2087  if ( ! $date_format ) {
2088  // If the format key isn't valid, return default format key
2089  return isset( $gf_date_formats[ $format ] ) ? $format : $default_format;
2090  }
2091 
2092  // If the format key isn't valid, return default format value
2093  return \GV\Utils::get( $gf_date_formats, $format, $gf_date_formats[ $default_format ] );
2094  }
2095 
2096  /**
2097  * If previewing a View or page with embedded Views, make the search work properly by adding hidden fields with query vars
2098  *
2099  * @since 2.2.1
2100  *
2101  * @return void
2102  */
2103  public function add_preview_inputs() {
2104  global $wp;
2105 
2106  if ( ! is_preview() || ! current_user_can( 'publish_gravityviews') ) {
2107  return;
2108  }
2109 
2110  // Outputs `preview` and `post_id` variables
2111  foreach ( $wp->query_vars as $key => $value ) {
2112  printf( '<input type="hidden" name="%s" value="%s" />', esc_attr( $key ), esc_attr( $value ) );
2113  }
2114 
2115  }
2116 
2117  /**
2118  * Get an operator URL override.
2119  *
2120  * @param array $get Where to look for the operator.
2121  * @param string $key The filter key to look for.
2122  * @param array $allowed The allowed operators (allowlist).
2123  * @param string $default The default operator.
2124  *
2125  * @return string The operator.
2126  */
2127  private function get_operator( $get, $key, $allowed, $default ) {
2128  $operator = \GV\Utils::get( $get, "$key|op", $default );
2129 
2130  /**
2131  * @depecated 2.14
2132  */
2133  $allowed = apply_filters_deprecated( 'gravityview/search/operator_whitelist', array( $allowed, $key ), '2.14', 'gravityview/search/operator_allowlist' );
2134 
2135  /**
2136  * @filter `gravityview/search/operator_allowlist` An array of allowed operators for a field.
2137  * @since 2.14
2138  * @param string[] An allowlist of operators.
2139  * @param string The filter name.
2140  */
2141  $allowed = apply_filters( 'gravityview/search/operator_allowlist', $allowed, $key );
2142 
2143  if ( ! in_array( $operator, $allowed, true ) ) {
2144  $operator = $default;
2145  }
2146 
2147  return $operator;
2148  }
2149 
2150 
2151 } // end class
2152 
2154 
2155 if ( ! gravityview()->plugin->supports( \GV\Plugin::FEATURE_GFQUERY ) ) {
2156  return;
2157 }
2158 
2159 /**
2160  * A GF_Query condition that allows user data searches.
2161  */
2162 class GravityView_Widget_Search_Author_GF_Query_Condition extends \GF_Query_Condition {
2163  public function __construct( $filter, $view ) {
2164  $this->value = $filter['value'];
2165  $this->view = $view;
2166  }
2167 
2168  public function sql( $query ) {
2169  global $wpdb;
2170 
2171  $user_meta_fields = array(
2172  'nickname', 'first_name', 'last_name',
2173  );
2174 
2175  /**
2176  * @filter `gravityview/widgets/search/created_by/user_meta_fields` Filter the user meta fields to search.
2177  * @param array The user meta fields.
2178  * @param \GV\View $view The view.
2179  */
2180  $user_meta_fields = apply_filters( 'gravityview/widgets/search/created_by/user_meta_fields', $user_meta_fields, $this->view );
2181 
2182  $user_fields = array(
2183  'user_nicename', 'user_login', 'display_name', 'user_email',
2184  );
2185 
2186  /**
2187  * @filter `gravityview/widgets/search/created_by/user_fields` Filter the user fields to search.
2188  * @param array The user fields.
2189  * @param \GV\View $view The view.
2190  */
2191  $user_fields = apply_filters( 'gravityview/widgets/search/created_by/user_fields', $user_fields, $this->view );
2192 
2193  $conditions = array();
2194 
2195  foreach ( $user_fields as $user_field ) {
2196  $conditions[] = $wpdb->prepare( "`u`.`$user_field` LIKE %s", '%' . $wpdb->esc_like( $this->value ) . '%' );
2197  }
2198 
2199  foreach ( $user_meta_fields as $meta_field ) {
2200  $conditions[] = $wpdb->prepare( "(`um`.`meta_key` = %s AND `um`.`meta_value` LIKE %s)", $meta_field, '%' . $wpdb->esc_like( $this->value ) . '%' );
2201  }
2202 
2203  $conditions = '(' . implode( ' OR ', $conditions ) . ')';
2204 
2205  $alias = $query->_alias( null );
2206 
2207  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)))";
2208  }
2209 }
new GravityView_Widget_Search
rgget_or_rgpost( $name)
Based on the search method, fetch the value for a specific key.
$url
Definition: post_image.php:25
sieve_filter_choices( $filter, $context)
Sieve filter choices to only ones that are used.
static get_field_label( $field, $form_field=array())
Get the label for a search form field.
$labels
static get_searchable_fields()
Ajax Returns the form fields ( only the searchable ones )
static _GET( $name, $default=null)
Grab a value from the _GET superglobal or default.
$forms
Definition: data-source.php:19
add_datepicker_localization( $localizations=array(), $view_data=array())
Modify the array passed to wp_localize_script()
static getInstance( $passed_post=NULL)
set_search_method()
Sets the search method to GET (default) or POST.
get_search_method()
Returns the search method.
get_search_filter_details( $field, $context, $widget_args)
Prepare search fields to frontend render with other details (label, field type, searched values) ...
static get_search_input_labels()
Get labels for different types of search bar inputs.
if(empty( $value)) $user
static _REQUEST( $name, $default=null)
Grab a value from the _REQUEST superglobal or default.
new GravityView_Cache
if(gv_empty( $field['value'], false, false)) $format
register_no_conflict( $allowed)
Add admin script to the no-conflict scripts allowlist.
static get_created_by_choices( $view)
Calculate the search choices for the users.
static get_formatted_date( $value='', $format='Y-m-d', $value_format='Y-m-d')
Format a date value.
add_preview_inputs()
If previewing a View or page with embedded Views, make the search work properly by adding hidden fiel...
enqueue_datepicker()
Enqueue the datepicker script.
gravityview_get_link( $href='', $anchor_text='', $atts=array())
Generate an HTML anchor tag with a list of supported attributes.
static get_search_form_action()
Calculate the search form action.
if(gravityview() ->plugin->is_GF_25()) $form
gravityview_get_input_id_from_id( $field_id='')
Very commonly needed: get the # of the input based on a full field ID.
static by_id( $form, $field_id)
Get a by and Field ID.
If this file is called directly, abort.
render_frontend( $widget_args, $content='', $context='')
Renders the Search Widget.
if(empty( $field_settings['content'])) $content
Definition: custom.php:37
static get_search_input_label( $input_type)
static get_date_field_format(GF_Field_Date $field)
Get the Field Format form GravityForms.
gv_map_deep( $value, $callback)
Maps a function to all non-iterable elements of an array or an object.
has_date_field( $search_fields)
Check whether the configured search fields have a date field.
gravityview_get_field( $form, $field_id)
Returns the field details array of a specific form given the field id.
maybe_sieve_filter_choices( $search_fields, $widget, $widget_args, $context)
If sieve choices is enabled, run it for each of the fields with choices.
gravityview_get_form_fields( $form='', $add_default_properties=false, $include_parent_field=true)
Return array of fields&#39; id and label, for a given Form ID.
static pre_get_form_fields( $template_id='')
Get the form fields for a preset (no form created yet)
Definition: class-ajax.php:319
$search_method
whether search method is GET or POST ( default: GET )
register_scripts()
Register search widget scripts, including Flexibility.
add_scripts_and_styles( $hook)
Add script to Views edit screen (admin)
static by_id( $post_id)
Construct a instance from a post ID.
static get_users( $context='change_entry_creator', $args=array())
Get WordPress users with reasonable limits set.
is_registered()
Whether this Widget&#39;s been registered already or not.
gravityview_get_form_id( $view_id)
Get the connected form ID from a View ID.
const GRAVITYVIEW_FILE
Full path to the GravityView file "GRAVITYVIEW_FILE" "./gravityview.php".
Definition: gravityview.php:40
static get_search_class( $custom_class='')
Get the search class for a search form.
gf_query_filter(&$query, $view, $request)
Filters the with advanced logic.
maybe_enqueue_flexibility()
If the current visitor is running IE 8 or 9, enqueue Flexibility.
add_datepicker_js_dependency( $js_dependencies)
Require the datepicker script for the frontend GV script.
static get_search_input_types( $field_id='', $field_type=null)
Assign an input type according to the form field type.
static by_id( $field_id)
Get a from an internal Gravity Forms field ID.
if(empty( $created_by)) $form_id
gravityview_get_terms_choices( $args=array())
Get categories formatted in a way used by GravityView and Gravity Forms input choices.
const UNAPPROVED
get_datepicker_format( $date_format=false)
Retrieve the datepicker format.
prepare_field_filter( $filter_key, $value, $view, $searchable_fields, $get=array())
Prepare the field filters to GFAPI.
convert_request_key_to_filter_key( $key)
Convert $_GET/$_POST key to the field/meta ID.
filter_entries( $search_criteria, $form_id=null, $args=array(), $force_search_criteria=false)
— Frontend —
get_operator( $get, $key, $allowed, $default)
Get an operator URL override.
static get( $array, $key, $default=null)
Grab a value from an array or an object or default.
A GF_Query condition that allows user data searches.
gravityview()
The main GravityView wrapper function.
static render_searchable_fields( $form_id=null, $current='')
Generates html for the available Search Fields dropdown.
gv_empty( $value, $zero_is_empty=true, $allow_string_booleans=true)
Is the value empty?
static get_all()
Return array of status options.
if(false !==strpos( $value, '00:00')) $field_id
string $field_id ID of the field being displayed
Definition: time.php:22
static get_is_approved_choices()
Calculate the search checkbox choices for approval status.
gravityview_get_permalink_query_args( $id=0)
Get get_permalink() without the home_url() prepended to it.
add_no_permalink_fields( $search_fields, $object, $widget_args=array())
Display hidden fields to add support for sites using Default permalink structure. ...
add_template_path( $file_paths)
Include this extension templates path.
get_view_searchable_fields( $view, $with_full_field=false)
Get the fields that are searchable for a View.
static the_clear_search_button()
Output the Clear Search Results button.
static get_input_types_by_field_type()
Get the input types available for different field types.
static getInstance()
Get the one true instantiated self.