-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathGFSearch Shortcode
More file actions
410 lines (365 loc) · 17.5 KB
/
GFSearch Shortcode
File metadata and controls
410 lines (365 loc) · 17.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
// 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. <br>
* sort_key: The field ID, entry property, or entry meta key to sort the results. <br>
* sort_direction: The direction to sort the results. Can be ASC, DESC, or RAND. Case-insensitive. Default is DESC <br>
* 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 <a> 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 = '<a target="_blank" href="' . admin_url( 'admin.php?page=gf_entries&view=entry&id=' . $entry['form_id'] . '&lid=' . $entry['id'] ) . '">' . $result_text . '</a>';
}
$results[] = $result_text;
} else {
$result_text = implode( ', ', $entry_results );
if ( $atts['link'] ) {
$result_text = '<a target="_blank" href="' . admin_url( 'admin.php?page=gf_entries&view=entry&id=' . $entry['form_id'] . '&lid=' . $entry['id'] ) . '">' . $result_text . '</a>';
}
$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'] );
}