// 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'] ); }