PluginProbe
Property Hive / 2.3.1
Property Hive v2.3.1
2.3.1 2.3.0 2.2.6 2.2.5 2.2.4 2.2.3 2.2.2 1.4.46 1.4.47 1.4.48 1.4.49 1.4.5 1.4.50 1.4.51 1.4.52 1.4.53 1.4.54 1.4.55 1.4.56 1.4.57 1.4.58 1.4.59 1.4.6 1.4.60 1.4.61 All 261 releases
← All changes | includes/admin/class-ph-admin-applicant-list.php +217 -91 2.2.42.3.1 View file →
@@ -1,5 +1,8 @@
1 - <?php
1 +<?php
2 +// phpcs:set WordPress.Security.ValidatedSanitizedInput customSanitizingFunctions[] ph_clean
3 +// ph_clean() recursively sanitizes text; presence, shape and unslashing checks remain separate.
4 +
2 5 /**
3 6 * PropertyHive Admin Generate Applicant List Class.
4 7 *
5 8 * @author PropertyHive
@@ -14,8 +17,9 @@
14 17
15 18 /**
16 19 * PH_Admin_Applicant_List
17 20 */
21 +// phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedClassFound -- Legacy public global class PH_Admin_Applicant_List; preserving the existing PH_* class name is required for plugin and extension compatibility.
18 22 class PH_Admin_Applicant_List {
19 23
20 24 /**
21 25 * Handles the display of the main Property Hive reports page in admin.
@@ -24,9 +28,36 @@
24 28 * @return void
25 29 */
26 30 public function output() {
27 31
28 - $property_types = array();
32 + // Applicant filters are read-only; the export endpoint verifies its nonce and capability.
33 + // phpcs:ignore WordPress.Security.NonceVerification.Missing -- This request only repopulates the read-only filter form and renders its results.
34 + $request_post = wp_unslash( $_POST );
35 + $has_department_input = isset( $request_post['department'] ) && is_scalar( $request_post['department'] );
36 + $department_input = $has_department_input ? sanitize_text_field( $request_post['department'] ) : '';
37 + $maximum_price_input = ( isset( $request_post['maximum_price'] ) && is_scalar( $request_post['maximum_price'] ) ) ? sanitize_text_field( $request_post['maximum_price'] ) : '';
38 + $maximum_rent_input = ( isset( $request_post['maximum_rent'] ) && is_scalar( $request_post['maximum_rent'] ) ) ? sanitize_text_field( $request_post['maximum_rent'] ) : '';
39 + $minimum_bedrooms_input = ( isset( $request_post['minimum_bedrooms'] ) && is_scalar( $request_post['minimum_bedrooms'] ) ) ? sanitize_text_field( $request_post['minimum_bedrooms'] ) : '';
40 + $property_types_input = array();
41 + if ( isset( $request_post['property_types'] ) && is_array( $request_post['property_types'] ) ) {
42 + foreach ( $request_post['property_types'] as $property_type_input ) {
43 + if ( is_scalar( $property_type_input ) ) {
44 + $property_types_input[] = absint( $property_type_input );
45 + }
46 + }
47 + }
48 + $locations_input = array();
49 + if ( isset( $request_post['locations'] ) && is_array( $request_post['locations'] ) ) {
50 + foreach ( $request_post['locations'] as $location_input ) {
51 + if ( is_scalar( $location_input ) ) {
52 + $locations_input[] = absint( $location_input );
53 + }
54 + }
55 + }
56 + $include_non_send_matching_properties_input = ( isset( $request_post['include_non_send_matching_properties'] ) && is_scalar( $request_post['include_non_send_matching_properties'] ) ) ? sanitize_text_field( $request_post['include_non_send_matching_properties'] ) : '';
57 + $submitted_applicant_list = ( isset( $request_post['submitted_applicant_list'] ) && is_scalar( $request_post['submitted_applicant_list'] ) ) ? sanitize_key( $request_post['submitted_applicant_list'] ) : '';
58 +
59 + $property_types = array();
29 60 $locations = array();
30 61 ?>
31 62 <div class="wrap propertyhive">
32 63
@@ -57,13 +88,15 @@
57 88
58 89 foreach ( $department_options as $key => $department )
59 90 {
60 91 echo '<option value="' . esc_attr($key) . '"';
61 - if ( isset($_POST['department']) && $_POST['department'] == $key )
92 + // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Read-only applicant filters and form display; CSV export separately verifies its nonce and CRM capability.
93 + if ( $has_department_input && $department_input == $key )
62 94 {
63 95 echo ' selected';
64 96 }
65 - elseif ( !isset($_POST['department']) && $key == get_option( 'propertyhive_primary_department' ) )
97 + // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Read-only applicant filters and form display; CSV export separately verifies its nonce and CRM capability.
98 + elseif ( ! $has_department_input && $key == get_option( 'propertyhive_primary_department' ) )
66 99 {
67 100 echo ' selected';
68 101 }
69 102 echo '>' . esc_html($department) . '</option>';
@@ -74,19 +107,25 @@
74 107 </p>
75 108
76 109 <p class="form-field sales-only">
77 110 <label><?php echo esc_html__( 'Maximum Price', 'propertyhive' ); ?> <img class="help_tip" data-tip="This will search the applicant's Match Price Range if one is set and return applicants where the price entered falls into this range. Otherwise it will search the Maximum Price and return applicants that have maximum price higher than the value entered" src="<?php echo esc_url(PH()->plugin_url()); ?>/assets/images/help.png" height="16" width="16" /></label>
78 - <input type="text" name="maximum_price" value="<?php if ( isset($_POST['maximum_price']) ) { echo esc_attr( $_POST['maximum_price'] ); } ?>">
111 + <input type="text" name="maximum_price" value="<?php
112 +// phpcs:ignore WordPress.Security.NonceVerification.Missing -- Read-only applicant filters and form display; CSV export separately verifies its nonce and CRM capability.
113 + if ( $maximum_price_input !== '' ) { echo esc_attr( $maximum_price_input ); } ?>">
79 114 </p>
80 115
81 116 <p class="form-field lettings-only">
82 117 <label><?php echo esc_html__( 'Maximum Rent (PCM)', 'propertyhive' ); ?></label>
83 - <input type="text" name="maximum_rent" value="<?php if ( isset($_POST['maximum_rent']) ) { echo esc_attr( $_POST['maximum_rent'] ); } ?>">
118 + <input type="text" name="maximum_rent" value="<?php
119 +// phpcs:ignore WordPress.Security.NonceVerification.Missing -- Read-only applicant filters and form display; CSV export separately verifies its nonce and CRM capability.
120 + if ( $maximum_rent_input !== '' ) { echo esc_attr( $maximum_rent_input ); } ?>">
84 121 </p>
85 122
86 123 <p class="form-field residential-only">
87 124 <label><?php echo esc_html__( 'Minimum Bedrooms', 'propertyhive' ); ?></label>
88 - <input type="number" name="minimum_bedrooms" class="short" value="<?php if ( isset($_POST['minimum_bedrooms']) ) { echo esc_attr( $_POST['minimum_bedrooms'] ); } ?>">
125 + <input type="number" name="minimum_bedrooms" class="short" value="<?php
126 +// phpcs:ignore WordPress.Security.NonceVerification.Missing -- Read-only applicant filters and form display; CSV export separately verifies its nonce and CRM capability.
127 + if ( $minimum_bedrooms_input !== '' ) { echo esc_attr( $minimum_bedrooms_input ); } ?>">
89 128 </p>
90 129
91 130 <p class="form-field residential-only">
92 131 <label><?php echo esc_html__( 'Property Types', 'propertyhive' ); ?></label>
@@ -96,18 +135,19 @@
96 135 $args = array(
97 136 'hide_empty' => false,
98 137 'parent' => 0
99 138 );
100 - $terms = get_terms( 'property_type', $args );
139 + $terms = get_terms( array_merge( wp_parse_args( $args ), array( 'taxonomy' => 'property_type' ) ) );
101 140
102 141 if ( !empty( $terms ) && !is_wp_error( $terms ) )
103 142 {
104 143 foreach ($terms as $term)
105 144 {
106 - $property_types[$term->term_id] = esc_html( $term->name );
145 + $property_types[$term->term_id] = $term->name;
107 146
108 147 echo '<option value="' . esc_attr( $term->term_id ) . '"';
109 - if ( isset($_POST['property_types']) && in_array( $term->term_id, $_POST['property_types'] ) )
148 + // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Read-only applicant filters and form display; CSV export separately verifies its nonce and CRM capability.
149 + if ( ! empty( $property_types_input ) && in_array( $term->term_id, $property_types_input ) )
110 150 {
111 151 echo ' selected';
112 152 }
113 153 echo '>' . esc_html( $term->name ) . '</option>';
@@ -115,18 +155,19 @@
115 155 $args = array(
116 156 'hide_empty' => false,
117 157 'parent' => $term->term_id
118 158 );
119 - $subterms = get_terms( 'property_type', $args );
159 + $subterms = get_terms( array_merge( wp_parse_args( $args ), array( 'taxonomy' => 'property_type' ) ) );
120 160
121 161 if ( !empty( $subterms ) && !is_wp_error( $subterms ) )
122 162 {
123 163 foreach ($subterms as $term)
124 164 {
125 - $property_types[$term->term_id] = esc_html( $term->name );
165 + $property_types[$term->term_id] = $term->name;
126 166
127 167 echo '<option value="' . esc_attr( $term->term_id ) . '"';
128 - if ( isset($_POST['property_types']) && in_array( $term->term_id, $_POST['property_types'] ) )
168 + // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Read-only applicant filters and form display; CSV export separately verifies its nonce and CRM capability.
169 + if ( ! empty( $property_types_input ) && in_array( $term->term_id, $property_types_input ) )
129 170 {
130 171 echo ' selected';
131 172 }
132 173 echo '>- ' . esc_html( $term->name ) . '</option>';
@@ -146,18 +187,19 @@
146 187 $args = array(
147 188 'hide_empty' => false,
148 189 'parent' => 0
149 190 );
150 - $terms = get_terms( 'location', $args );
191 + $terms = get_terms( array_merge( wp_parse_args( $args ), array( 'taxonomy' => 'location' ) ) );
151 192
152 193 if ( !empty( $terms ) && !is_wp_error( $terms ) )
153 194 {
154 195 foreach ($terms as $term)
155 196 {
156 - $locations[$term->term_id] = esc_html( $term->name );
197 + $locations[$term->term_id] = $term->name;
157 198
158 199 echo '<option value="' . esc_attr( $term->term_id ) . '"';
159 - if ( isset($_POST['locations']) && in_array( $term->term_id, $_POST['locations'] ) )
200 + // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Read-only applicant filters and form display; CSV export separately verifies its nonce and CRM capability.
201 + if ( ! empty( $locations_input ) && in_array( $term->term_id, $locations_input ) )
160 202 {
161 203 echo ' selected';
162 204 }
163 205 echo '>' . esc_html( $term->name ) . '</option>';
@@ -165,18 +207,19 @@
165 207 $args = array(
166 208 'hide_empty' => false,
167 209 'parent' => $term->term_id
168 210 );
169 - $subterms = get_terms( 'location', $args );
211 + $subterms = get_terms( array_merge( wp_parse_args( $args ), array( 'taxonomy' => 'location' ) ) );
170 212
171 213 if ( !empty( $subterms ) && !is_wp_error( $subterms ) )
172 214 {
173 215 foreach ($subterms as $term)
174 216 {
175 - $locations[$term->term_id] = esc_html( $term->name );
217 + $locations[$term->term_id] = $term->name;
176 218
177 219 echo '<option value="' . esc_attr( $term->term_id ) . '"';
178 - if ( isset($_POST['locations']) && in_array( $term->term_id, $_POST['locations'] ) )
220 + // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Read-only applicant filters and form display; CSV export separately verifies its nonce and CRM capability.
221 + if ( ! empty( $locations_input ) && in_array( $term->term_id, $locations_input ) )
179 222 {
180 223 echo ' selected';
181 224 }
182 225 echo '>- ' . esc_html( $term->name ) . '</option>';
@@ -184,18 +227,19 @@
184 227 $args = array(
185 228 'hide_empty' => false,
186 229 'parent' => $term->term_id
187 230 );
188 - $subsubterms = get_terms( 'location', $args );
231 + $subsubterms = get_terms( array_merge( wp_parse_args( $args ), array( 'taxonomy' => 'location' ) ) );
189 232
190 233 if ( !empty( $subsubterms ) && !is_wp_error( $subsubterms ) )
191 234 {
192 235 foreach ($subsubterms as $term)
193 236 {
194 - $locations[$term->term_id] = esc_html( $term->name );
237 + $locations[$term->term_id] = $term->name;
195 238
196 239 echo '<option value="' . esc_attr( $term->term_id ) . '"';
197 - if ( isset($_POST['locations']) && in_array( $term->term_id, $_POST['locations'] ) )
240 + // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Read-only applicant filters and form display; CSV export separately verifies its nonce and CRM capability.
241 + if ( ! empty( $locations_input ) && in_array( $term->term_id, $locations_input ) )
198 242 {
199 243 echo ' selected';
200 244 }
201 245 echo '>- - ' . esc_html( $term->name ) . '</option>';
@@ -210,9 +254,11 @@
210 254 </p>
211 255
212 256 <p class="form-field">
213 257 <label><?php echo esc_html__( 'Include Applicants with \'Send Matching Properties\' Unticked', 'propertyhive' ); ?></label>
214 - <input type="checkbox" name="include_non_send_matching_properties" value="yes"<?php if ( isset($_POST['include_non_send_matching_properties']) && sanitize_text_field($_POST['include_non_send_matching_properties']) == 'yes' ) { echo ' checked'; } ?>>
258 + <input type="checkbox" name="include_non_send_matching_properties" value="yes"<?php
259 +// phpcs:ignore WordPress.Security.NonceVerification.Missing -- Read-only applicant filters and form display; CSV export separately verifies its nonce and CRM capability.
260 + if ( $include_non_send_matching_properties_input !== '' && sanitize_text_field($include_non_send_matching_properties_input) == 'yes' ) { echo ' checked'; } ?>>
215 261 </p>
216 262
217 263 <?php do_action('propertyhive_applicant_list_additional_fields'); ?>
218 264
@@ -228,9 +274,10 @@
228 274
229 275 <div class="applicant-list-results">
230 276
231 277 <?php
232 - if ( isset($_POST['submitted_applicant_list']) && $_POST['submitted_applicant_list'] == '1' )
278 + // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Read-only applicant filters and form display; CSV export separately verifies its nonce and CRM capability.
279 + if ( $submitted_applicant_list !== '' && $submitted_applicant_list == '1' )
233 280 {
234 281 $results = $this->generate_results();
235 282 ?>
236 283 <br>
@@ -264,9 +311,9 @@
264 311 }
265 312 }
266 313 ?>
267 314 <tr>
268 - <td><a href="<?php echo esc_attr($result['edit_link']); ?>" target="_blank"><?php echo esc_html($result['name']); ?></a></td>
315 + <td><a href="<?php echo esc_url($result['edit_link']); ?>" target="_blank"><?php echo esc_html($result['name']); ?></a></td>
269 316 <td><?php
270 317 $contact_details = array();
271 318 if ( $result['telephone_number'] != '' )
272 319 {
@@ -275,8 +322,9 @@
275 322 if ( $result['email_address'] != '' )
276 323 {
277 324 $contact_details[] = 'E: ' . esc_html($result['email_address']);
278 325 }
326 + // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Telephone and email text are escaped above; only fixed br markup joins them.
279 327 echo !empty($contact_details) ? implode("<br>", $contact_details) : '-';
280 328 ?></td>
281 329 <td><?php
282 330 if ( isset($result['profile']['department']) )
@@ -296,9 +344,9 @@
296 344 if ( !isset($result['profile']['match_price_range_lower_actual']) || ( isset($result['profile']['match_price_range_lower_actual']) && $result['profile']['match_price_range_lower_actual'] == '' ) )
297 345 {
298 346 if ( isset($result['profile']['max_price']) && $result['profile']['max_price'] != '' )
299 347 {
300 - $match_price_range_lower = $result['profile']['max_price'] - ( $result['profile']['max_price'] * ( $percentage_lower / 100 ) );
348 + $match_price_range_lower = (float) $result['profile']['max_price'] - ( (float) $result['profile']['max_price'] * ( (float) $percentage_lower / 100 ) );
301 349 }
302 350 }
303 351 else
304 352 {
@@ -309,9 +357,9 @@
309 357 if ( !isset($result['profile']['match_price_range_higher_actual']) || ( isset($result['profile']['match_price_range_higher_actual']) && $result['profile']['match_price_range_higher_actual'] == '' ) )
310 358 {
311 359 if ( isset($result['profile']['max_price']) && $result['profile']['max_price'] != '' )
312 360 {
313 - $match_price_range_higher = $result['profile']['max_price'] + ( $result['profile']['max_price'] * ( $percentage_higher / 100 ) );
361 + $match_price_range_higher = (float) $result['profile']['max_price'] + ( (float) $result['profile']['max_price'] * ( (float) $percentage_higher / 100 ) );
314 362 }
315 363 }
316 364 else
317 365 {
@@ -327,9 +375,9 @@
327 375 }
328 376 }
329 377 if ( isset($result['profile']['min_beds']) && $result['profile']['min_beds'] != '' && $result['profile']['min_beds'] != 0 )
330 378 {
331 - $output[] = '<strong>Min Beds:</strong> ' . esc_html(number_format($result['profile']['min_beds']));
379 + $output[] = '<strong>Min Beds:</strong> ' . esc_html(number_format( (float) $result['profile']['min_beds'] ));
332 380 }
333 381 if ( isset($result['profile']['property_types']) && is_array($result['profile']['property_types']) && !empty($result['profile']['property_types']) )
334 382 {
335 383 $output_types = array();
@@ -334,10 +382,9 @@
334 382 {
335 383 $output_types = array();
336 384 foreach ( $result['profile']['property_types'] as $profile_type )
337 385 {
338 - if ( isset($property_types[$profile_type]) )
339 - {
386 + if ( is_scalar( $profile_type ) && isset( $property_types[$profile_type] ) ) {
340 387 $output_types[] = $property_types[$profile_type];
341 388 }
342 389 }
343 390 $output[] = '<strong>Property Types:</strong> ' . esc_html(implode(", ", $output_types));
@@ -346,10 +393,9 @@
346 393 {
347 394 $output_locations = array();
348 395 foreach ( $result['profile']['locations'] as $profile_location )
349 396 {
350 - if ( isset($locations[$profile_location]) )
351 - {
397 + if ( is_scalar( $profile_location ) && isset( $locations[$profile_location] ) ) {
352 398 $output_locations[] = $locations[$profile_location];
353 399 }
354 400 }
355 401 $output[] = '<strong>Locations:</strong> ' . esc_html(implode(", ", $output_locations));
@@ -357,8 +403,9 @@
357 403 if ( isset($result['profile']['notes']) && $result['profile']['notes'] != '' )
358 404 {
359 405 $output[] = '<strong>Additional Requirements:</strong> ' . nl2br(esc_html($result['profile']['notes']));
360 406 }
407 + // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Requirement text is escaped when assembled above; strong/br markup is fixed and currency-symbol filter HTML remains trusted.
361 408 echo( !empty($output) ? implode("<br>", $output) : '-' );
362 409 break;
363 410 }
364 411 case "residential-lettings":
@@ -369,9 +416,9 @@
369 416 $output[] = '<strong>Max Rent:</strong> ' . $currency . esc_html(ph_display_price_field($result['profile']['max_rent']) . $result['profile']['rent_frequency']);
370 417 }
371 418 if ( isset($result['profile']['min_beds']) && $result['profile']['min_beds'] != '' && $result['profile']['min_beds'] != 0 )
372 419 {
373 - $output[] = '<strong>Min Beds:</strong> ' . esc_html(number_format($result['profile']['min_beds']));
420 + $output[] = '<strong>Min Beds:</strong> ' . esc_html(number_format( (float) $result['profile']['min_beds'] ));
374 421 }
375 422 if ( isset($result['profile']['property_types']) && is_array($result['profile']['property_types']) && !empty($result['profile']['property_types']) )
376 423 {
377 424 $output_types = array();
@@ -376,10 +423,9 @@
376 423 {
377 424 $output_types = array();
378 425 foreach ( $result['profile']['property_types'] as $profile_type )
379 426 {
380 - if ( isset($property_types[$profile_type]) )
381 - {
427 + if ( is_scalar( $profile_type ) && isset( $property_types[$profile_type] ) ) {
382 428 $output_types[] = $property_types[$profile_type];
383 429 }
384 430 }
385 431 $output[] = '<strong>Property Types:</strong> ' . esc_html(implode(", ", $output_types));
@@ -388,10 +434,9 @@
388 434 {
389 435 $output_locations = array();
390 436 foreach ( $result['profile']['locations'] as $profile_location )
391 437 {
392 - if ( isset($locations[$profile_location]) )
393 - {
438 + if ( is_scalar( $profile_location ) && isset( $locations[$profile_location] ) ) {
394 439 $output_locations[] = $locations[$profile_location];
395 440 }
396 441 }
397 442 $output[] = '<strong>Locations:</strong> ' . esc_html(implode(", ", $output_locations));
@@ -399,8 +444,9 @@
399 444 if ( isset($result['profile']['notes']) && $result['profile']['notes'] != '' )
400 445 {
401 446 $output[] = '<strong>Additional Requirements:</strong> ' . nl2br(esc_html($result['profile']['notes']));
402 447 }
448 + // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Requirement text is escaped when assembled above; strong/br markup is fixed and currency-symbol filter HTML remains trusted.
403 449 echo( !empty($output) ? implode("<br>", $output) : '-' );
404 450 break;
405 451 }
406 452 case "commercial":
@@ -410,9 +456,11 @@
410 456 {
411 457 $output_locations = array();
412 458 foreach ( $result['profile']['locations'] as $profile_location )
413 459 {
414 - $output_locations[] = $locations[$profile_location];
460 + if ( is_scalar( $profile_location ) && isset( $locations[$profile_location] ) ) {
461 + $output_locations[] = $locations[$profile_location];
462 + }
415 463 }
416 464 $output[] = '<strong>Locations:</strong> ' . esc_html(implode(", ", $output_locations));
417 465 }
418 466 if ( isset($result['profile']['notes']) && $result['profile']['notes'] != '' )
@@ -418,8 +466,9 @@
418 466 if ( isset($result['profile']['notes']) && $result['profile']['notes'] != '' )
419 467 {
420 468 $output[] = '<strong>Additional Requirements:</strong> ' . nl2br(esc_html($result['profile']['notes']));
421 469 }
470 + // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Requirement text is escaped when assembled above; strong/br markup is fixed and currency-symbol filter HTML remains trusted.
422 471 echo( !empty($output) ? implode("<br>", $output) : '-' );
423 472 break;
424 473 }
425 474 }
@@ -526,22 +575,57 @@
526 575 }
527 576
528 577 public function generate_results()
529 578 {
579 + // Results are read-only. Normalize the submitted filters before they are used in comparisons or queries.
580 + // phpcs:ignore WordPress.Security.NonceVerification.Missing -- This method only reads applicant filters; the separate export endpoint verifies its nonce and capability.
581 + $request_post = wp_unslash( $_POST );
582 + $has_department = isset( $request_post['department'] ) && is_scalar( $request_post['department'] );
583 + $department_input = $has_department ? sanitize_text_field( $request_post['department'] ) : '';
584 + $maximum_price_input = ( isset( $request_post['maximum_price'] ) && is_scalar( $request_post['maximum_price'] ) ) ? sanitize_text_field( $request_post['maximum_price'] ) : '';
585 + $maximum_rent_input = ( isset( $request_post['maximum_rent'] ) && is_scalar( $request_post['maximum_rent'] ) ) ? sanitize_text_field( $request_post['maximum_rent'] ) : '';
586 + $minimum_bedrooms_input = ( isset( $request_post['minimum_bedrooms'] ) && is_scalar( $request_post['minimum_bedrooms'] ) ) ? sanitize_text_field( $request_post['minimum_bedrooms'] ) : '';
587 + $has_property_types = isset( $request_post['property_types'] ) && is_array( $request_post['property_types'] );
588 + $property_types_input = array();
589 + if ( $has_property_types ) {
590 + foreach ( $request_post['property_types'] as $property_type_input ) {
591 + if ( is_scalar( $property_type_input ) ) {
592 + $property_types_input[] = absint( $property_type_input );
593 + }
594 + }
595 + }
596 + $has_locations = isset( $request_post['locations'] ) && is_array( $request_post['locations'] );
597 + $locations_input = array();
598 + if ( $has_locations ) {
599 + foreach ( $request_post['locations'] as $location_input ) {
600 + if ( is_scalar( $location_input ) ) {
601 + $locations_input[] = absint( $location_input );
602 + }
603 + }
604 + }
605 + $has_include_non_send_matching_properties = isset( $request_post['include_non_send_matching_properties'] );
606 +
530 607 $search_property_types = array();
531 608 if (
532 - isset($_POST['department']) &&
609 + // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Read-only applicant filters and form display; CSV export separately verifies its nonce and CRM capability.
610 + $has_department &&
533 611 (
534 - $_POST['department'] == 'residential-sales' ||
535 - $_POST['department'] == 'residential-lettings' ||
536 - ph_get_custom_department_based_on($_POST['department']) == 'residential-sales' ||
537 - ph_get_custom_department_based_on($_POST['department']) == 'residential-lettings'
612 + // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Read-only applicant filters and form display; CSV export separately verifies its nonce and CRM capability.
613 + $department_input == 'residential-sales' ||
614 + // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Read-only applicant filters and form display; CSV export separately verifies its nonce and CRM capability.
615 + $department_input == 'residential-lettings' ||
616 + // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Read-only applicant filters and form display; CSV export separately verifies its nonce and CRM capability.
617 + ph_get_custom_department_based_on($department_input) == 'residential-sales' ||
618 + // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Read-only applicant filters and form display; CSV export separately verifies its nonce and CRM capability.
619 + ph_get_custom_department_based_on($department_input) == 'residential-lettings'
538 620 )
539 621 )
540 622 {
541 - if ( isset($_POST['property_types']) && is_array($_POST['property_types']) && !empty($_POST['property_types']) )
623 + // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Read-only applicant filters and form display; CSV export separately verifies its nonce and CRM capability.
624 + if ( $has_property_types && ! empty( $property_types_input ) )
542 625 {
543 - foreach ( $_POST['property_types'] as $property_type )
626 + // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Read-only applicant filters and form display; CSV export separately verifies its nonce and CRM capability.
627 + foreach ( $property_types_input as $property_type )
544 628 {
545 629 $search_property_types[] = (int)$property_type;
546 630
547 631 $args = array(
@@ -547,9 +631,9 @@
547 631 $args = array(
548 632 'hide_empty' => false,
549 633 'parent' => $property_type
550 634 );
551 - $terms = get_terms( 'property_type', $args );
635 + $terms = get_terms( array_merge( wp_parse_args( $args ), array( 'taxonomy' => 'property_type' ) ) );
552 636
553 637 if ( !empty( $terms ) && !is_wp_error( $terms ) )
554 638 {
555 639 foreach ($terms as $term)
@@ -559,9 +643,9 @@
559 643 $args = array(
560 644 'hide_empty' => false,
561 645 'parent' => $term->term_id
562 646 );
563 - $subterms = get_terms( 'property_type', $args );
647 + $subterms = get_terms( array_merge( wp_parse_args( $args ), array( 'taxonomy' => 'property_type' ) ) );
564 648
565 649 if ( !empty( $subterms ) && !is_wp_error( $subterms ) )
566 650 {
567 651 foreach ($subterms as $term)
@@ -576,11 +660,13 @@
576 660 $search_property_types = array_unique($search_property_types);
577 661 }
578 662
579 663 $search_locations = array();
580 - if ( isset($_POST['locations']) && is_array($_POST['locations']) && !empty($_POST['locations']) )
664 + // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Read-only applicant filters and form display; CSV export separately verifies its nonce and CRM capability.
665 + if ( $has_locations && ! empty( $locations_input ) )
581 666 {
582 - foreach ( $_POST['locations'] as $location )
667 + // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Read-only applicant filters and form display; CSV export separately verifies its nonce and CRM capability.
668 + foreach ( $locations_input as $location )
583 669 {
584 670 $search_locations[] = (int)$location;
585 671
586 672 $args = array(
@@ -586,9 +672,9 @@
586 672 $args = array(
587 673 'hide_empty' => false,
588 674 'parent' => $location
589 675 );
590 - $terms = get_terms( 'location', $args );
676 + $terms = get_terms( array_merge( wp_parse_args( $args ), array( 'taxonomy' => 'location' ) ) );
591 677
592 678 if ( !empty( $terms ) && !is_wp_error( $terms ) )
593 679 {
594 680 foreach ($terms as $term)
@@ -598,9 +684,9 @@
598 684 $args = array(
599 685 'hide_empty' => false,
600 686 'parent' => $term->term_id
601 687 );
602 - $subterms = get_terms( 'location', $args );
688 + $subterms = get_terms( array_merge( wp_parse_args( $args ), array( 'taxonomy' => 'location' ) ) );
603 689
604 690 if ( !empty( $subterms ) && !is_wp_error( $subterms ) )
605 691 {
606 692 foreach ($subterms as $term)
@@ -619,8 +705,9 @@
619 705 'fields' => 'ids',
620 706 'nopaging' => true,
621 707 );
622 708
709 + // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- Existing applicant membership is stored in serialized _contact_types metadata; preserve complete list/export results and their extension match checks. Query fetches IDs only.
623 710 $args['meta_query'] = array();
624 711
625 712 $args['meta_query'][] = array(
626 713 'key' => '_contact_types',
@@ -652,9 +739,10 @@
652 739 $profile = get_post_meta( get_the_ID(), '_applicant_profile_' . $i, TRUE );
653 740
654 741 $match = true;
655 742
656 - if ( !isset($_POST['include_non_send_matching_properties']) )
743 + // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Read-only applicant filters and form display; CSV export separately verifies its nonce and CRM capability.
744 + if ( !$has_include_non_send_matching_properties )
657 745 {
658 746 if ( !isset($profile['send_matching_properties']) || ( isset($profile['send_matching_properties']) && $profile['send_matching_properties'] != 'yes' ) )
659 747 {
660 748 $match = false;
@@ -660,21 +748,26 @@
660 748 $match = false;
661 749 }
662 750 }
663 751
664 - if ( isset($_POST['department']) )
752 + // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Read-only applicant filters and form display; CSV export separately verifies its nonce and CRM capability.
753 + if ( $has_department )
665 754 {
666 - if ( isset($profile['department']) && $profile['department'] != ph_clean($_POST['department']) )
755 + // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Read-only applicant filters and form display; CSV export separately verifies its nonce and CRM capability.
756 + if ( isset($profile['department']) && $profile['department'] != ph_clean($department_input) )
667 757 {
668 758 $match = false;
669 759 }
670 760 }
671 761
672 - if ( isset($_POST['department']) && ( $_POST['department'] == 'residential-sales' || ph_get_custom_department_based_on($_POST['department']) == 'residential-sales' ) )
762 + // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Read-only applicant filters and form display; CSV export separately verifies its nonce and CRM capability.
763 + if ( $has_department && ( $department_input == 'residential-sales' || ph_get_custom_department_based_on($department_input) == 'residential-sales' ) )
673 764 {
674 - if ( isset($_POST['maximum_price']) && ph_clean($_POST['maximum_price']) != '' )
765 + // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Read-only applicant filters and form display; CSV export separately verifies its nonce and CRM capability.
766 + if ( $maximum_price_input !== '' && ph_clean($maximum_price_input) != '' )
675 767 {
676 - $price = preg_replace("/[^0-9.]/", '', ph_clean($_POST['maximum_price']));
768 + // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Read-only applicant filters and form display; CSV export separately verifies its nonce and CRM capability.
769 + $price = preg_replace("/[^0-9.]/", '', ph_clean($maximum_price_input));
677 770
678 771 if ( $percentage_lower != '' && $percentage_higher != '' )
679 772 {
680 773 $match_price_range_lower = '';
@@ -731,13 +824,16 @@
731 824 }
732 825 }
733 826 }
734 827 }
735 - if ( isset($_POST['department']) && ( $_POST['department'] == 'residential-lettings' || ph_get_custom_department_based_on($_POST['department']) == 'residential-lettings' ) )
828 + // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Read-only applicant filters and form display; CSV export separately verifies its nonce and CRM capability.
829 + if ( $has_department && ( $department_input == 'residential-lettings' || ph_get_custom_department_based_on($department_input) == 'residential-lettings' ) )
736 830 {
737 - if ( isset($_POST['maximum_rent']) && ph_clean($_POST['maximum_rent']) != '' )
831 + // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Read-only applicant filters and form display; CSV export separately verifies its nonce and CRM capability.
832 + if ( $maximum_rent_input !== '' && ph_clean($maximum_rent_input) != '' )
738 833 {
739 - $price = preg_replace("/[^0-9.]/", '', ph_clean($_POST['maximum_rent']));
834 + // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Read-only applicant filters and form display; CSV export separately verifies its nonce and CRM capability.
835 + $price = preg_replace("/[^0-9.]/", '', ph_clean($maximum_rent_input));
740 836
741 837 if ( isset($profile['max_price_actual']) && $profile['max_price_actual'] != '' && $profile['max_price_actual'] != 0 && $profile['max_price_actual'] < $price )
742 838 {
743 839 $match = false;
@@ -744,20 +840,27 @@
744 840 }
745 841 }
746 842 }
747 843 if (
748 - isset($_POST['department']) &&
844 + // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Read-only applicant filters and form display; CSV export separately verifies its nonce and CRM capability.
845 + $has_department &&
749 846 (
750 - $_POST['department'] == 'residential-sales' ||
751 - $_POST['department'] == 'residential-lettings' ||
752 - ph_get_custom_department_based_on($_POST['department']) == 'residential-sales' ||
753 - ph_get_custom_department_based_on($_POST['department']) == 'residential-lettings'
847 + // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Read-only applicant filters and form display; CSV export separately verifies its nonce and CRM capability.
848 + $department_input == 'residential-sales' ||
849 + // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Read-only applicant filters and form display; CSV export separately verifies its nonce and CRM capability.
850 + $department_input == 'residential-lettings' ||
851 + // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Read-only applicant filters and form display; CSV export separately verifies its nonce and CRM capability.
852 + ph_get_custom_department_based_on($department_input) == 'residential-sales' ||
853 + // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Read-only applicant filters and form display; CSV export separately verifies its nonce and CRM capability.
854 + ph_get_custom_department_based_on($department_input) == 'residential-lettings'
754 855 )
755 856 )
756 857 {
757 - if ( isset($_POST['minimum_bedrooms']) && ph_clean($_POST['minimum_bedrooms']) != '' )
858 + // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Read-only applicant filters and form display; CSV export separately verifies its nonce and CRM capability.
859 + if ( $minimum_bedrooms_input !== '' && ph_clean($minimum_bedrooms_input) != '' )
758 860 {
759 - $beds = preg_replace("/[^0-9.]/", '', ph_clean($_POST['minimum_bedrooms']));
861 + // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Read-only applicant filters and form display; CSV export separately verifies its nonce and CRM capability.
862 + $beds = preg_replace("/[^0-9.]/", '', ph_clean($minimum_bedrooms_input));
760 863
761 864 if ( isset($profile['min_beds']) && $profile['min_beds'] != '' && $profile['min_beds'] != 0 && $profile['min_beds'] > $beds )
762 865 {
763 866 $match = false;
@@ -764,9 +867,10 @@
764 867 }
765 868 }
766 869
767 870 // Property Types
768 - if ( isset($_POST['property_types']) && is_array($_POST['property_types']) && !empty($_POST['property_types']) )
871 + // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Read-only applicant filters and form display; CSV export separately verifies its nonce and CRM capability.
872 + if ( $has_property_types && ! empty( $property_types_input ) )
769 873 {
770 874 $found_type = false;
771 875 foreach ( $search_property_types as $search_property_type )
772 876 {
@@ -782,9 +886,10 @@
782 886 }
783 887 }
784 888 }
785 889
786 - if ( isset($_POST['locations']) && is_array($_POST['locations']) && !empty($_POST['locations']) )
890 + // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Read-only applicant filters and form display; CSV export separately verifies its nonce and CRM capability.
891 + if ( $has_locations && ! empty( $locations_input ) )
787 892 {
788 893 $found_type = false;
789 894 foreach ( $search_locations as $search_location )
790 895 {
@@ -827,44 +932,50 @@
827 932 }
828 933
829 934 private function array_2_csv($results)
830 935 {
936 + // export() verifies the nonce and capability before calling this formatter.
937 + // phpcs:ignore WordPress.Security.NonceVerification.Missing -- This private formatter only reads the already-authorized export filters.
938 + $request_post = wp_unslash( $_POST );
939 + $has_department = isset( $request_post['department'] ) && is_scalar( $request_post['department'] );
940 + $department_input = $has_department ? sanitize_text_field( $request_post['department'] ) : '';
941 +
831 942 $locations = array();
832 943 $args = array(
833 944 'hide_empty' => false,
834 945 'parent' => 0
835 946 );
836 - $terms = get_terms( 'location', $args );
947 + $terms = get_terms( array_merge( wp_parse_args( $args ), array( 'taxonomy' => 'location' ) ) );
837 948
838 949 if ( !empty( $terms ) && !is_wp_error( $terms ) )
839 950 {
840 951 foreach ($terms as $term)
841 952 {
842 - $locations[$term->term_id] = esc_html( $term->name );
953 + $locations[$term->term_id] = html_entity_decode( $term->name, ENT_QUOTES, get_bloginfo( 'charset' ) );
843 954
844 955 $args = array(
845 956 'hide_empty' => false,
846 957 'parent' => $term->term_id
847 958 );
848 - $subterms = get_terms( 'location', $args );
959 + $subterms = get_terms( array_merge( wp_parse_args( $args ), array( 'taxonomy' => 'location' ) ) );
849 960
850 961 if ( !empty( $subterms ) && !is_wp_error( $subterms ) )
851 962 {
852 963 foreach ($subterms as $term)
853 964 {
854 - $locations[$term->term_id] = esc_html( $term->name );
965 + $locations[$term->term_id] = html_entity_decode( $term->name, ENT_QUOTES, get_bloginfo( 'charset' ) );
855 966
856 967 $args = array(
857 968 'hide_empty' => false,
858 969 'parent' => $term->term_id
859 970 );
860 - $subsubterms = get_terms( 'location', $args );
971 + $subsubterms = get_terms( array_merge( wp_parse_args( $args ), array( 'taxonomy' => 'location' ) ) );
861 972
862 973 if ( !empty( $subsubterms ) && !is_wp_error( $subsubterms ) )
863 974 {
864 975 foreach ($subsubterms as $term)
865 976 {
866 - $locations[$term->term_id] = esc_html( $term->name );
977 + $locations[$term->term_id] = html_entity_decode( $term->name, ENT_QUOTES, get_bloginfo( 'charset' ) );
867 978 }
868 979 }
869 980 }
870 981 }
@@ -875,27 +986,27 @@
875 986 $args = array(
876 987 'hide_empty' => false,
877 988 'parent' => 0
878 989 );
879 - $terms = get_terms( 'property_type', $args );
990 + $terms = get_terms( array_merge( wp_parse_args( $args ), array( 'taxonomy' => 'property_type' ) ) );
880 991
881 992 if ( !empty( $terms ) && !is_wp_error( $terms ) )
882 993 {
883 994 foreach ($terms as $term)
884 995 {
885 - $property_types[$term->term_id] = esc_html( $term->name );
996 + $property_types[$term->term_id] = html_entity_decode( $term->name, ENT_QUOTES, get_bloginfo( 'charset' ) );
886 997
887 998 $args = array(
888 999 'hide_empty' => false,
889 1000 'parent' => $term->term_id
890 1001 );
891 - $subterms = get_terms( 'property_type', $args );
1002 + $subterms = get_terms( array_merge( wp_parse_args( $args ), array( 'taxonomy' => 'property_type' ) ) );
892 1003
893 1004 if ( !empty( $subterms ) && !is_wp_error( $subterms ) )
894 1005 {
895 1006 foreach ($subterms as $term)
896 1007 {
897 - $property_types[$term->term_id] = esc_html( $term->name );
1008 + $property_types[$term->term_id] = html_entity_decode( $term->name, ENT_QUOTES, get_bloginfo( 'charset' ) );
898 1009 }
899 1010 }
900 1011 }
901 1012 }
@@ -914,11 +1025,11 @@
914 1025 'address' => __( 'Address', 'propertyhive' ),
915 1026 'department' => __( 'Department', 'propertyhive' ),
916 1027 );
917 1028
918 - if ( isset($_POST['department']) )
1029 + if ( $has_department )
919 1030 {
920 - $department = ph_clean($_POST['department']);
1031 + $department = ph_clean( $department_input );
921 1032 if ( ph_get_custom_department_based_on($department) !== FALSE )
922 1033 {
923 1034 $department = ph_get_custom_department_based_on($department);
924 1035 }
@@ -929,8 +1040,11 @@
929 1040 {
930 1041 case "residential-sales":
931 1042 {
932 1043 $columns['maximum_price'] = __( 'Maximum Price', 'propertyhive' );
1044 + if ( $percentage_lower != '' && $percentage_higher != '' ) {
1045 + $columns['maximum_price_range'] = __( 'Maximum Price Range', 'propertyhive' );
1046 + }
933 1047 break;
934 1048 }
935 1049 case "residential-lettings":
936 1050 {
@@ -948,11 +1062,13 @@
948 1062
949 1063 $columns['locations'] = __( 'Locations', 'propertyhive' );
950 1064 $columns['additional_requirements'] = __( 'Additional Requirements', 'propertyhive' );
951 1065
1066 + // Keep the historical raw POST value as the filter contract for extensions.
1067 + // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Export nonce and capability are verified by export() before this private formatter is called; the raw value is retained for extension compatibility.
952 1068 $columns = apply_filters( 'propertyhive_export_applicant_list_columns', $columns, $_POST );
953 1069
954 - fputcsv($df, $columns);
1070 + fputcsv( $df, $columns, ',', '"', '' );
955 1071
956 1072 foreach ($results as $result)
957 1073 {
958 1074 $columns = array(
@@ -959,9 +1075,9 @@
959 1075 'name' => $result['name'],
960 1076 'email_address' => $result['email_address'],
961 1077 'telephone_number' => $result['telephone_number'],
962 1078 'address' => $result['address'],
963 - 'department' => ( isset($result['profile']['department']) ? __( ucwords(str_replace("-", " ", $result['profile']['department'])), 'propertyhive' ) : '-' ),
1079 + 'department' => ( isset($result['profile']['department']) ? propertyhive_get_department_label( $result['profile']['department'] ) : '-' ),
964 1080 );
965 1081
966 1082 if ( isset($department) )
967 1083 {
@@ -969,8 +1085,11 @@
969 1085 {
970 1086 case "residential-sales":
971 1087 {
972 1088 $columns['maximum_price'] = ( isset($result['profile']['max_price']) ? $result['profile']['max_price'] : '' );
1089 + if ( $percentage_lower != '' && $percentage_higher != '' ) {
1090 + $columns['maximum_price_range'] = '';
1091 + }
973 1092
974 1093 if ( !empty($columns['maximum_price']) )
975 1094 {
976 1095 if ( $percentage_lower != '' && $percentage_higher != '' )
@@ -979,9 +1098,9 @@
979 1098 if ( !isset($result['profile']['match_price_range_lower_actual']) || ( isset($result['profile']['match_price_range_lower_actual']) && $result['profile']['match_price_range_lower_actual'] == '' ) )
980 1099 {
981 1100 if ( isset($result['profile']['max_price']) && $result['profile']['max_price'] != '' )
982 1101 {
983 - $match_price_range_lower = $result['profile']['max_price'] - ( $result['profile']['max_price'] * ( $percentage_lower / 100 ) );
1102 + $match_price_range_lower = (float) $result['profile']['max_price'] - ( (float) $result['profile']['max_price'] * ( (float) $percentage_lower / 100 ) );
984 1103 }
985 1104 }
986 1105 else
987 1106 {
@@ -992,9 +1111,9 @@
992 1111 if ( !isset($result['profile']['match_price_range_higher_actual']) || ( isset($result['profile']['match_price_range_higher_actual']) && $result['profile']['match_price_range_higher_actual'] == '' ) )
993 1112 {
994 1113 if ( isset($result['profile']['max_price']) && $result['profile']['max_price'] != '' )
995 1114 {
996 - $match_price_range_higher = $result['profile']['max_price'] + ( $result['profile']['max_price'] * ( $percentage_higher / 100 ) );
1115 + $match_price_range_higher = (float) $result['profile']['max_price'] + ( (float) $result['profile']['max_price'] * ( (float) $percentage_higher / 100 ) );
997 1116 }
998 1117 }
999 1118 else
1000 1119 {
@@ -1029,9 +1148,11 @@
1029 1148 if ( isset($result['profile']['property_types']) && is_array($result['profile']['property_types']) && !empty($result['profile']['property_types']) )
1030 1149 {
1031 1150 foreach ( $result['profile']['property_types'] as $profile_type )
1032 1151 {
1033 - $output_types[] = $property_types[$profile_type];
1152 + if ( is_scalar( $profile_type ) && isset( $property_types[$profile_type] ) ) {
1153 + $output_types[] = $property_types[$profile_type];
1154 + }
1034 1155 }
1035 1156 }
1036 1157 $columns['property_types'] = implode(", ", $output_types);
1037 1158 }
@@ -1041,9 +1162,11 @@
1041 1162 if ( isset($result['profile']['locations']) && is_array($result['profile']['locations']) && !empty($result['profile']['locations']) )
1042 1163 {
1043 1164 foreach ( $result['profile']['locations'] as $profile_location )
1044 1165 {
1045 - $output_locations[] = $locations[$profile_location];
1166 + if ( is_scalar( $profile_location ) && isset( $locations[$profile_location] ) ) {
1167 + $output_locations[] = $locations[$profile_location];
1168 + }
1046 1169 }
1047 1170 }
1048 1171 $columns['locations'] = implode(", ", $output_locations);
1049 1172
@@ -1048,13 +1171,15 @@
1048 1171 $columns['locations'] = implode(", ", $output_locations);
1049 1172
1050 1173 $columns['additional_requirements'] = isset($result['profile']['notes']) ? $result['profile']['notes'] : '';
1051 1174
1175 + // Keep the historical raw POST value as the filter contract for extensions.
1176 + // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Export nonce and capability are verified by export() before this private formatter is called; the raw value is retained for extension compatibility.
1052 1177 $columns = apply_filters( 'propertyhive_export_applicant_list_row_data', $columns, $_POST, $result['contact_id'], $result['applicant_profile_id'] );
1053 1178
1054 - fputcsv($df, $columns);
1179 + fputcsv( $df, $columns, ',', '"', '' );
1055 1180 }
1056 - fclose($df);
1181 + fclose($df); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose -- Closes the php://output CSV stream.
1057 1182
1058 1183 return ob_get_clean();
1059 1184 }
1060 1185
@@ -1069,9 +1194,9 @@
1069 1194 {
1070 1195 wp_die( esc_html__( 'Insufficient permissions', 'propertyhive' ), 403 );
1071 1196 }
1072 1197
1073 - $filename = 'applicant-list-' . date("YmdHis") . '.csv';
1198 + $filename = 'applicant-list-' . gmdate("YmdHis") . '.csv';
1074 1199
1075 1200 // disable caching
1076 1201 $now = gmdate("D, d M Y H:i:s");
1077 1202 header("Expires: Tue, 03 Jul 2001 06:00:00 GMT");
@@ -1088,8 +1213,9 @@
1088 1213 header("Content-Transfer-Encoding: binary");
1089 1214
1090 1215 $results = $this->generate_results();
1091 1216
1217 + // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Attachment response is CSV encoded by fputcsv, not HTML; HTML escaping would corrupt exported values.
1092 1218 echo $this->array_2_csv($results);
1093 1219
1094 1220 die();
1095 1221 }
@@ -1094,5 +1220,5 @@
1094 1220 die();
1095 1221 }
1096 1222 }
1097 1223
1098 -endif;
1224 +endif;