// version DEPRECATED. SEE PLUGIN INSTEAD
add_action(
'init',
function () {
add_shortcode( 'gfsearch', 'gfsearch_shortcode' );
}
);
/**
* Processes the gfsearch shortcode to perform searching and displaying Gravity Forms entries
* based on specified criteria and attributes.
*
* Notes:
* This method allows searching for specific forms, multiple forms, or all forms. Custom formatting,
* sorting, filtering, limiting results, and handling specific search conditions is supported.
* Detailed formatting instructions are outlined above.
*
* Supported attributes include search fields, result limits, sorting directions, numeric comparisons,
* and unique results handling.
*
* @param array $atts An associative array of attributes, or default values.
*
* @param string $content Content of the shortcode, typically search values separated by '|'.
*
* @return string|false Formatted search results or false if search fails due to missing attributes or invalid setup.
*/
function gfsearch_shortcode( $atts, $content = null ) {
/**
* Notes:
*
* For the target use 0 to search all forms or a form ID to search a specific form or a comma separated list of form IDs to
* search the specified forms.
*
* You can pass multiple id's to the search and display attributes, separated by a comma, in order to search or display multiple fields. If you are searching
* for multiple fields enter the corresponding value as the content for the shortcode with each value to search for separated by a | symbol. Make sure you have the
* same amount of values as fields you are searching and make sure they are in the same order.
*
* If you want custom formating for the display: Configure the attribute with the format you would like for the display and surround each entry property by curly braces
* i.e. display="This is example text before one field: {13} and this is some more ({14}), and this-{15} is the last field!). Each id {13}, {14}, and {15} will be replaced by
* the correct value and the rest of the string will stay the same. Just make sure not to enter any characters that would break the shortcode such as " or []. Limited HTML is allowed.
* Any entry property key can be used as a placeholder to be replaced with the value. For example, you can use {id} or {created_by} or a field id {13}, etc. See https://docs.gravityforms.com/entry-object/.
*
* When using this shortcode with Gravity View, you may need to prefix non-numeric keys with "gfs:" to prevent Gravity View from parsing them as merge tags.
* For example, use {gfs:id} instead of {id} when working with Gravity View. Both formats are supported by this shortcode.
*
* The search and display fields can be a field ID, entry property, or entry meta key.
*
* If you are searching for multiple values (fields) in the same entry you can use the search_mode attribute to determine if the entry must meet all the conditions
* or not. Default is all conditions. If you pass in the value any (search_mode="any") then the result will be returned if any condition matches.
*
* To perform a global search on the form for any field with the specified value, leave the corresponding search id blank. To just display the values from a field leave out the search attribute and the
* shortcode content.
*
* To check for multiple values for one field enter the field multiple times in the search attribute, with the desired values separated by a comma as the shortcode content and set the
* search_mode attribute to "any".
*
* If you would like to search for results where the value is greater or less than the provided search value use the greater_than or less_than attributes.
* The attribute expects the field id first and then the number to filter by separated by a space and comma. For example greater_than="4, 500" will filter out
* all entries where field 4 has a value of less than 500.
*
* If you want to sort the entries use the sort_key, sort_direction, and sort_is_num.
* sort_key: The field ID, entry property, or entry meta key to sort the results.
* sort_direction: The direction to sort the results. Can be ASC, DESC, or RAND. Case-insensitive. Default is DESC
* sort_is_num: Indicates if the values of the specified key are numeric. Should be used in conjunction with the sort_key attribute. Default is true.
* To set to false use the string false (sort_is_num="false") or an empty value such as 0 or an empty string.
*
* If you want to have a secondary sort within the first use the secondary_sort_key and secondary_sort_direction attributes. They work similar to the primary sorting attributes, the only difference being
* there is no random option for the sorting direction. There is also no "is_numeric" attribute. It is unnecessary here. Note also this attribute will be ignored if the primary sort direction is RAND.
*
* If you only want unique values use the attribute unique and give it any value (aside from 0 or an empty string).
*
* If you want to return a specific amount of results use the limit attribute. The default is one result. If you want to display all the results use the
* value 'all' (limit="all"), case-insensitive. If you enter a number greater than the total amount of results all of them will be returned. If you enter 0 or an empty string
* the default value will be used.
*
* You can specify the separator between results with the separator attribute (i.e. separator=<br>). Limited HTML (such as <br>) is allowed here.
*
* If you want to search for empty values, meaning where the specified field in the search attribute is empty, leave the content of the shortcode blank
* and use the search_empty attribute. You can give it any non-empty value (0, empty string, etc.). The default is false so if there is a search field
* with no value nothing will be returned.
*
* If you want to specify a default value to display when no results are found, use the default attribute (i.e. default="No results found").
* This value will be displayed if either no entries match the search criteria or if all entries are filtered out during processing.
* The default value is also used for individual blank values within entries. For example, if multiple entries are returned and some have
* values for the display fields while others don't, or if multiple fields are being displayed and some have values while others don't,
* the default value will be used for those individual blank values.
*
* If you want to turn each result into a link to the relevant entry in the admin panel, use the link attribute with any non-empty value
* (i.e. link="true"). This will wrap each result in an HTML anchor tag that links to the entry view page in the WordPress admin.
*/
$result = apply_filters( 'gogv_shortcode_process', $content );
if ( $result !== $content ) {
return $result;
}
$atts = shortcode_atts(
[
'target' => '0',
'search' => '',
'greater_than' => false,
'less_than' => false,
'display' => '',
'sort_key' => 'id',
'sort_direction' => 'DESC',
'sort_is_num' => true,
'secondary_sort_key' => '',
'secondary_sort_direction' => 'DESC',
'unique' => false,
'limit' => '1',
'search_mode' => 'all',
'separator' => '',
'search_empty' => false,
'default' => '',
'link' => false,
],
$atts,
'gfsearch'
);
// Allow everything wp_kses_post allows plus and its attributes
$allowed_tags = wp_kses_allowed_html( 'post' );
$allowed_tags['a'] = [
'href' => true,
'title' => true,
'target' => true,
'rel' => true,
'class' => true,
'id' => true,
'style' => true,
];
$content = html_entity_decode( $content, ENT_QUOTES );
$form_id = array_map( 'intval', explode( ',', $atts['target'] ) );
$search_criteria = [];
$search_criteria['status'] = 'active';
$search_criteria['field_filters'] = [];
$search_criteria['field_filters']['mode'] = in_array( strtolower( $atts['search_mode'] ), [ 'all', 'any' ], true ) ? strtolower( $atts['search_mode'] ) : 'all';
if ( ! empty( $atts['search'] ) && empty( $atts['display'] ) && ! $atts['search_empty'] ) {
return '';
}
$search_ids = array_map( 'sanitize_text_field', explode( ',', $atts['search'] ) );
$search_ids = array_map( 'trim', $search_ids );
$content_values = array_map( 'trim', explode( '|', $content ) );
foreach ( $search_ids as $index => $search_id ) {
if ( empty( $search_id ) ) {
continue;
}
$current_field = GFAPI::get_field( $form_id[0], $search_id );
if ( 'number' === $current_field['type'] ) {
$content_values[ $index ] = str_replace( ',', '', $content_values[ $index ] );
}
$search_criteria['field_filters'][] = [
'key' => $search_id,
'value' => GFCommon::replace_variables( $content_values[ $index ], [], [] ),
];
}
$sorting = [
'key' => sanitize_text_field( $atts['sort_key'] ),
'direction' => in_array( strtoupper( $atts['sort_direction'] ), [ 'ASC', 'DESC', 'RAND' ], true ) ? strtoupper( $atts['sort_direction'] ) : 'DESC',
'is_numeric' => ! ( strtolower( $atts['sort_is_num'] ) === 'false' ) && $atts['sort_is_num'],
];
$secondary_sort_key = sanitize_text_field( $atts['secondary_sort_key'] );
$secondary_sort_direction = in_array( strtoupper( $atts['secondary_sort_direction'] ), [ 'ASC', 'DESC' ], true )
? strtoupper( $atts['secondary_sort_direction'] )
: 'DESC';
$paging_offset = 0;
$total_count = 0;
if ( 'all' !== strtolower( $atts['limit'] ) ) {
$original_limit = empty( $atts['limit'] ) ? 1 : (int) $atts['limit'];
if ( $secondary_sort_key ) {
$atts['limit'] = 'all';
}
}
if ( empty( $atts['limit'] ) ) {
$page_size = 1;
} elseif ( 'all' === strtolower( $atts['limit'] ) ) {
$page_size = 25;
} else {
$page_size = min( intVal( $atts['limit'] ), 25 );
}
$paging = [
'offset' => $paging_offset,
'page_size' => $page_size,
];
$entries = GFAPI::get_entries( $form_id, $search_criteria, $sorting, $paging, $total_count );
if ( 'all' === $atts['limit'] || intVal( $atts['limit'] ) > 25 ) {
$count = count( $entries );
while ( $total_count > $count ) {
$paging_offset += 25;
$paging = [
'offset' => $paging_offset,
'page_size' => 25,
];
$new_entries = GFAPI::get_entries( $form_id, $search_criteria, $sorting, $paging, $total_count );
array_push( $entries, ...$new_entries ); // $entries = array_merge( $entries, $new_entries );
if ( is_numeric( $atts['limit'] ) && count( $entries ) > $atts['limit'] ) {
break;
}
}
if ( is_numeric( $atts['limit'] ) ) {
$entries = array_slice( $entries, 0, intVal( $atts['limit'] ) );
}
}
if ( empty( $entries ) ) {
return wp_kses_post( $atts['default'] );
}
if ( ! empty( $secondary_sort_key ) && 'RAND' !== $sorting['direction'] ) {
$grouped_entries = [];
foreach ( $entries as $entry ) {
$primary_key_value = $entry[ $sorting['key'] ] ?? ''; // Use the primary sort key as the group key
$grouped_entries[ $primary_key_value ][] = $entry;
}
// Sort each group based on the secondary sort key
foreach ( $grouped_entries as &$group ) {
usort(
$group,
function ( $entry1, $entry2 ) use ( $secondary_sort_key, $secondary_sort_direction ) {
$value1 = $entry1[ $secondary_sort_key ] ?? '';
$value2 = $entry2[ $secondary_sort_key ] ?? '';
// For non-numeric values, use string comparison
if ( ! is_numeric( $value1 ) || ! is_numeric( $value2 ) ) {
if ( strtoupper( $secondary_sort_direction ) === 'ASC' ) {
return strcasecmp( $value1, $value2 ); // Ascending order for strings
}
return strcasecmp( $value2, $value1 ); // Descending order for strings
}
// If numeric, compare numerically
$value1 = (float) $value1;
$value2 = (float) $value2;
if ( strtoupper( $secondary_sort_direction ) === 'ASC' ) {
return $value1 <=> $value2; // Ascending order for numbers
}
return $value2 <=> $value1; // Descending order for numbers
}
);
}
unset( $group ); // Clean up the reference variable to avoid potential bugs
// Flatten groups back into a single array, retaining primary sort order
$entries = [];
foreach ( $grouped_entries as $group ) {
$entries = array_merge( $entries, $group );
}
}
if ( isset( $original_limit ) && $original_limit < count( $entries ) ) {
$entries = array_slice( $entries, 0, $original_limit );
}
if ( $atts['greater_than'] ) {
$greater_than = array_map( 'trim', explode( ',', $atts['greater_than'] ) );
$entries = array_filter(
$entries,
function ( $entry ) use ( $greater_than ) {
if ( $entry[ intval( $greater_than[0] ) ] > floatval( $greater_than[1] ) ) {
return true;
}
return false;
}
);
}
if ( $atts['less_than'] ) {
$less_than = array_map( 'trim', explode( ',', $atts['less_than'] ) );
$entries = array_filter(
$entries,
function ( $entry ) use ( $less_than ) {
if ( $entry[ intval( $less_than[0] ) ] < floatval( $less_than[1] ) ) {
return true;
}
return false;
}
);
}
$results = [];
$regex = '/{(gfs:)?([^{}]+)}/';
preg_match_all( $regex, $atts['display'], $matches );
if ( empty( $matches[0] ) ) {
$display_ids = array_map( 'sanitize_text_field', explode( ',', $atts['display'] ) );
$display_ids = array_map( 'trim', $display_ids );
} else {
// Extract the actual IDs, removing the prefix if present
$display_ids = array_map(
function ( $individual_match ) {
// Remove the curly braces
$content = str_replace( [ '{', '}' ], '', $individual_match );
// Remove the gfs: prefix if present
return str_replace( 'gfs:', '', $content );
},
$matches[0]
);
$display_ids = array_map( 'sanitize_text_field', $display_ids );
}
$multi_input_present = false;
foreach ( $entries as $entry ) {
$entry_results = [];
foreach ( $display_ids as $display_id ) {
$field = GFAPI::get_field( $entry['form_id'], $display_id );
// phpcs:disable WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
if ( 'number' === $field->type ) {
$field_value = GFCommon::format_number( $entry[ $display_id ], $field->numberFormat, $entry['currency'], true );
} elseif ( 'date' === $field->type ) {
$field_value = GFCommon::date_display( $entry[ $display_id ], 'Y-m-d', $field->dateFormat );
} elseif ( is_multi_input_field( $field ) ) {
$multi_input_present = true;
$ids = array_column( $field['inputs'], 'id' );
$field_results = [];
foreach ( $ids as $id ) {
if ( ! empty( $entry[ $id ] ) ) {
$field_results[] = $entry[ $id ];
}
}
$field_value = implode( ' ', $field_results );
} else {
$field_value = $entry[ $display_id ];
}
// Use default value if field value is empty
if ( '' === $field_value || is_null( $field_value ) ) {
$field_value = wp_kses_post( $atts['default'] );
}
$entry_results[ $display_id ] = $field_value;
}
// We only need to filter if the default value is empty
if ( '' === $atts['default'] || is_null( $atts['default'] ) ) {
$entry_results = array_filter( $entry_results, fn( $value ) => '' !== $value && ! is_null( $value ) );
}
if ( ! empty( $matches[0] ) ) {
$display_format = wp_kses( $atts['display'], $allowed_tags );
foreach ( $display_ids as $display_id ) {
// Replace both {id} and {gfs:id} formats with the value
// If the field was filtered out (because default was empty), use empty string
$value = $entry_results[ $display_id ] ?? '';
$display_format = str_replace( '{' . $display_id . '}', $value, $display_format );
$display_format = str_replace( '{gfs:' . $display_id . '}', $value, $display_format );
}
$result_text = $display_format;
if ( $atts['link'] ) {
$result_text = '' . $result_text . '';
}
$results[] = $result_text;
} else {
$result_text = implode( ', ', $entry_results );
if ( $atts['link'] ) {
$result_text = '' . $result_text . '';
}
$results[] = $result_text;
}
}
if ( $atts['unique'] ) {
$results = array_unique( $results );
}
$results = array_map( 'trim', $results );
$results = array_filter( $results, fn( $value ) => '' !== $value && ! is_null( $value ) );
if ( empty( $results ) ) {
return wp_kses_post( $atts['default'] );
}
if ( empty( $atts['separator'] ) ) {
$separator = ( count( $display_ids ) > 1 || $multi_input_present ) ? '; ' : ', ';
} else {
$separator = wp_kses_post( $atts['separator'] );
}
return wp_kses( implode( $separator, $results ), $allowed_tags );
}
/**
* Determines if a given field is a multi-input field.
*
* @param mixed $field The field configuration array. Expected to contain 'type' and optionally 'inputType' keys.
*
* @return bool True if the field is a multi-input field, false otherwise.
*/
function is_multi_input_field( $field ): bool {
return 'name' === $field['type'] || 'address' === $field['type'] || 'checkbox' === $field['type'] || ( ( 'image_choice' === $field['type'] || 'multi_choice' === $field['type'] ) && 'checkbox' === $field['inputType'] );
}