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
propertyhive / includes / admin / class-ph-admin-applicant-list.php

class-ph-admin-applicant-list.php in Property Hive 2.3.1, at includes/admin/class-ph-admin-applicant-list.php

1,225 lines 69.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
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
5 /**
6 * PropertyHive Admin Generate Applicant List Class.
7 *
8 * @author PropertyHive
9 * @category Admin
10 * @package PropertyHive/Admin
11 * @version 1.0.0
12 */
13
14 if ( ! defined( 'ABSPATH' ) ) exit; // Exit if accessed directly
15
16 if ( ! class_exists( 'PH_Admin_Applicant_List' ) ) :
17
18 /**
19 * PH_Admin_Applicant_List
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.
22 class PH_Admin_Applicant_List {
23
24 /**
25 * Handles the display of the main Property Hive reports page in admin.
26 *
27 * @access public
28 * @return void
29 */
30 public function output() {
31
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();
60 $locations = array();
61 ?>
62 <div class="wrap propertyhive">
63
64 <h1>Generate Applicant List</h1>
65
66 <form method="post" id="mainform" action="" class="applicant-list-form">
67
68 <input type="hidden" name="submitted_applicant_list" value="1">
69 <?php wp_nonce_field( 'ph_applicant_export', 'ph_applicant_export_nonce' ); ?>
70
71 <div id="poststuff" class="propertyhive_meta_box">
72
73 <p class="form-field">
74 <label><?php echo esc_html__( 'Looking For', 'propertyhive' ); ?></label>
75 <select name="department">
76 <?php
77
78 $departments = ph_get_departments();
79
80 $department_options = array();
81 foreach ( $departments as $key => $value )
82 {
83 if ( get_option( 'propertyhive_active_departments_' . str_replace("residential-", "", $key) ) == 'yes' )
84 {
85 $department_options[$key] = $value;
86 }
87 }
88
89 foreach ( $department_options as $key => $department )
90 {
91 echo '<option value="' . esc_attr($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 )
94 {
95 echo ' selected';
96 }
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' ) )
99 {
100 echo ' selected';
101 }
102 echo '>' . esc_html($department) . '</option>';
103 }
104 ?>
105
106 </select>
107 </p>
108
109 <p class="form-field sales-only">
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>
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 ); } ?>">
114 </p>
115
116 <p class="form-field lettings-only">
117 <label><?php echo esc_html__( 'Maximum Rent (PCM)', 'propertyhive' ); ?></label>
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 ); } ?>">
121 </p>
122
123 <p class="form-field residential-only">
124 <label><?php echo esc_html__( 'Minimum Bedrooms', 'propertyhive' ); ?></label>
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 ); } ?>">
128 </p>
129
130 <p class="form-field residential-only">
131 <label><?php echo esc_html__( 'Property Types', 'propertyhive' ); ?></label>
132 <select id="property_types" name="property_types[]" multiple="multiple" data-placeholder="<?php echo esc_attr__( 'Start typing to add property types', 'propertyhive' ); ?>..." class="multiselect attribute_values">
133 <?php
134 $options = array( '' => '' );
135 $args = array(
136 'hide_empty' => false,
137 'parent' => 0
138 );
139 $terms = get_terms( array_merge( wp_parse_args( $args ), array( 'taxonomy' => 'property_type' ) ) );
140
141 if ( !empty( $terms ) && !is_wp_error( $terms ) )
142 {
143 foreach ($terms as $term)
144 {
145 $property_types[$term->term_id] = $term->name;
146
147 echo '<option value="' . esc_attr( $term->term_id ) . '"';
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 ) )
150 {
151 echo ' selected';
152 }
153 echo '>' . esc_html( $term->name ) . '</option>';
154
155 $args = array(
156 'hide_empty' => false,
157 'parent' => $term->term_id
158 );
159 $subterms = get_terms( array_merge( wp_parse_args( $args ), array( 'taxonomy' => 'property_type' ) ) );
160
161 if ( !empty( $subterms ) && !is_wp_error( $subterms ) )
162 {
163 foreach ($subterms as $term)
164 {
165 $property_types[$term->term_id] = $term->name;
166
167 echo '<option value="' . esc_attr( $term->term_id ) . '"';
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 ) )
170 {
171 echo ' selected';
172 }
173 echo '>- ' . esc_html( $term->name ) . '</option>';
174 }
175 }
176 }
177 }
178 ?>
179 </select>
180 </p>
181
182 <p class="form-field">
183 <label><?php echo esc_html__( 'Location', 'propertyhive' ); ?></label>
184 <select id="locations" name="locations[]" multiple="multiple" data-placeholder="<?php echo esc_attr__( 'Start typing to add locations', 'propertyhive' ); ?>..." class="multiselect attribute_values">
185 <?php
186 $options = array( '' => '' );
187 $args = array(
188 'hide_empty' => false,
189 'parent' => 0
190 );
191 $terms = get_terms( array_merge( wp_parse_args( $args ), array( 'taxonomy' => 'location' ) ) );
192
193 if ( !empty( $terms ) && !is_wp_error( $terms ) )
194 {
195 foreach ($terms as $term)
196 {
197 $locations[$term->term_id] = $term->name;
198
199 echo '<option value="' . esc_attr( $term->term_id ) . '"';
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 ) )
202 {
203 echo ' selected';
204 }
205 echo '>' . esc_html( $term->name ) . '</option>';
206
207 $args = array(
208 'hide_empty' => false,
209 'parent' => $term->term_id
210 );
211 $subterms = get_terms( array_merge( wp_parse_args( $args ), array( 'taxonomy' => 'location' ) ) );
212
213 if ( !empty( $subterms ) && !is_wp_error( $subterms ) )
214 {
215 foreach ($subterms as $term)
216 {
217 $locations[$term->term_id] = $term->name;
218
219 echo '<option value="' . esc_attr( $term->term_id ) . '"';
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 ) )
222 {
223 echo ' selected';
224 }
225 echo '>- ' . esc_html( $term->name ) . '</option>';
226
227 $args = array(
228 'hide_empty' => false,
229 'parent' => $term->term_id
230 );
231 $subsubterms = get_terms( array_merge( wp_parse_args( $args ), array( 'taxonomy' => 'location' ) ) );
232
233 if ( !empty( $subsubterms ) && !is_wp_error( $subsubterms ) )
234 {
235 foreach ($subsubterms as $term)
236 {
237 $locations[$term->term_id] = $term->name;
238
239 echo '<option value="' . esc_attr( $term->term_id ) . '"';
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 ) )
242 {
243 echo ' selected';
244 }
245 echo '>- - ' . esc_html( $term->name ) . '</option>';
246 }
247 }
248 }
249 }
250 }
251 }
252 ?>
253 </select>
254 </p>
255
256 <p class="form-field">
257 <label><?php echo esc_html__( 'Include Applicants with \'Send Matching Properties\' Unticked', 'propertyhive' ); ?></label>
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'; } ?>>
261 </p>
262
263 <?php do_action('propertyhive_applicant_list_additional_fields'); ?>
264
265 <p class="form-field">
266 <input type="submit" value="<?php echo esc_attr(__( 'Generate Applicant List', 'propertyhive' )); ?>" class="button-primary">
267 <a href="" class="button" id="export_applicant_list_results_button"><?php echo esc_html__( 'Export To CSV', 'propertyhive' ); ?></a>
268 <input type="hidden" name="export_applicant_list_results" value="">
269 </p>
270
271 </div>
272
273 </form>
274
275 <div class="applicant-list-results">
276
277 <?php
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' )
280 {
281 $results = $this->generate_results();
282 ?>
283 <br>
284 <div class="applicant-list-results">
285 <h3><?php echo esc_html(number_format(count($results))); ?> Applicants Found Matching Your Criteria</h3>
286 <table width="100%" cellpadding="8" cellspacing="0">
287 <thead>
288 <tr>
289 <th style="text-align:left;"><?php echo esc_html__( 'Applicant Name', 'propertyhive' ); ?></th>
290 <th style="text-align:left;"><?php echo esc_html__( 'Contact Details', 'propertyhive' ); ?></th>
291 <th style="text-align:left;"><?php echo esc_html__( 'Requirements', 'propertyhive' ); ?></th>
292 </tr>
293 </thead>
294 <tbody>
295 <?php
296 if ( !empty($results) )
297 {
298 $percentage_lower = get_option( 'propertyhive_applicant_match_price_range_percentage_lower', '' );
299 $percentage_higher = get_option( 'propertyhive_applicant_match_price_range_percentage_higher', '' );
300
301 foreach ( $results as $result )
302 {
303 $currency = '&pound;';
304 if ( isset($result['profile']['currency']) && !empty($result['profile']['currency']) )
305 {
306 $PH_Countries = new PH_Countries();
307 $selected_currency = $PH_Countries->get_currency($result['profile']['currency']);
308 if ( $selected_currency !== false )
309 {
310 $currency = $selected_currency['currency_symbol'];
311 }
312 }
313 ?>
314 <tr>
315 <td><a href="<?php echo esc_url($result['edit_link']); ?>" target="_blank"><?php echo esc_html($result['name']); ?></a></td>
316 <td><?php
317 $contact_details = array();
318 if ( $result['telephone_number'] != '' )
319 {
320 $contact_details[] = 'T: ' . esc_html($result['telephone_number']);
321 }
322 if ( $result['email_address'] != '' )
323 {
324 $contact_details[] = 'E: ' . esc_html($result['email_address']);
325 }
326 // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Telephone and email text are escaped above; only fixed br markup joins them.
327 echo !empty($contact_details) ? implode("<br>", $contact_details) : '-';
328 ?></td>
329 <td><?php
330 if ( isset($result['profile']['department']) )
331 {
332 switch ( $result['profile']['department'] )
333 {
334 case "residential-sales":
335 {
336 $output = array();
337 if ( isset($result['profile']['max_price']) && $result['profile']['max_price'] != '' && $result['profile']['max_price'] != 0 )
338 {
339 $output[] = '<strong>Max Price:</strong> ' . $currency . esc_html(ph_display_price_field($result['profile']['max_price']));
340
341 if ( $percentage_lower != '' && $percentage_higher != '' )
342 {
343 $match_price_range_lower = '';
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'] == '' ) )
345 {
346 if ( isset($result['profile']['max_price']) && $result['profile']['max_price'] != '' )
347 {
348 $match_price_range_lower = (float) $result['profile']['max_price'] - ( (float) $result['profile']['max_price'] * ( (float) $percentage_lower / 100 ) );
349 }
350 }
351 else
352 {
353 $match_price_range_lower = $result['profile']['match_price_range_lower'];
354 }
355
356 $match_price_range_higher = '';
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'] == '' ) )
358 {
359 if ( isset($result['profile']['max_price']) && $result['profile']['max_price'] != '' )
360 {
361 $match_price_range_higher = (float) $result['profile']['max_price'] + ( (float) $result['profile']['max_price'] * ( (float) $percentage_higher / 100 ) );
362 }
363 }
364 else
365 {
366 $match_price_range_higher = $result['profile']['match_price_range_higher'];
367 }
368
369 if (
370 $match_price_range_lower != '' && $match_price_range_higher != ''
371 )
372 {
373 $output[] = '<strong>Max Price Range:</strong> ' . $currency . esc_html(ph_display_price_field($match_price_range_lower)) . ' to ' . $currency . esc_html(ph_display_price_field($match_price_range_higher));
374 }
375 }
376 }
377 if ( isset($result['profile']['min_beds']) && $result['profile']['min_beds'] != '' && $result['profile']['min_beds'] != 0 )
378 {
379 $output[] = '<strong>Min Beds:</strong> ' . esc_html(number_format( (float) $result['profile']['min_beds'] ));
380 }
381 if ( isset($result['profile']['property_types']) && is_array($result['profile']['property_types']) && !empty($result['profile']['property_types']) )
382 {
383 $output_types = array();
384 foreach ( $result['profile']['property_types'] as $profile_type )
385 {
386 if ( is_scalar( $profile_type ) && isset( $property_types[$profile_type] ) ) {
387 $output_types[] = $property_types[$profile_type];
388 }
389 }
390 $output[] = '<strong>Property Types:</strong> ' . esc_html(implode(", ", $output_types));
391 }
392 if ( isset($result['profile']['locations']) && is_array($result['profile']['locations']) && !empty($result['profile']['locations']) )
393 {
394 $output_locations = array();
395 foreach ( $result['profile']['locations'] as $profile_location )
396 {
397 if ( is_scalar( $profile_location ) && isset( $locations[$profile_location] ) ) {
398 $output_locations[] = $locations[$profile_location];
399 }
400 }
401 $output[] = '<strong>Locations:</strong> ' . esc_html(implode(", ", $output_locations));
402 }
403 if ( isset($result['profile']['notes']) && $result['profile']['notes'] != '' )
404 {
405 $output[] = '<strong>Additional Requirements:</strong> ' . nl2br(esc_html($result['profile']['notes']));
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.
408 echo( !empty($output) ? implode("<br>", $output) : '-' );
409 break;
410 }
411 case "residential-lettings":
412 {
413 $output = array();
414 if ( isset($result['profile']['max_rent']) && $result['profile']['max_rent'] != '' && $result['profile']['max_rent'] != 0 )
415 {
416 $output[] = '<strong>Max Rent:</strong> ' . $currency . esc_html(ph_display_price_field($result['profile']['max_rent']) . $result['profile']['rent_frequency']);
417 }
418 if ( isset($result['profile']['min_beds']) && $result['profile']['min_beds'] != '' && $result['profile']['min_beds'] != 0 )
419 {
420 $output[] = '<strong>Min Beds:</strong> ' . esc_html(number_format( (float) $result['profile']['min_beds'] ));
421 }
422 if ( isset($result['profile']['property_types']) && is_array($result['profile']['property_types']) && !empty($result['profile']['property_types']) )
423 {
424 $output_types = array();
425 foreach ( $result['profile']['property_types'] as $profile_type )
426 {
427 if ( is_scalar( $profile_type ) && isset( $property_types[$profile_type] ) ) {
428 $output_types[] = $property_types[$profile_type];
429 }
430 }
431 $output[] = '<strong>Property Types:</strong> ' . esc_html(implode(", ", $output_types));
432 }
433 if ( isset($result['profile']['locations']) && is_array($result['profile']['locations']) && !empty($result['profile']['locations']) )
434 {
435 $output_locations = array();
436 foreach ( $result['profile']['locations'] as $profile_location )
437 {
438 if ( is_scalar( $profile_location ) && isset( $locations[$profile_location] ) ) {
439 $output_locations[] = $locations[$profile_location];
440 }
441 }
442 $output[] = '<strong>Locations:</strong> ' . esc_html(implode(", ", $output_locations));
443 }
444 if ( isset($result['profile']['notes']) && $result['profile']['notes'] != '' )
445 {
446 $output[] = '<strong>Additional Requirements:</strong> ' . nl2br(esc_html($result['profile']['notes']));
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.
449 echo( !empty($output) ? implode("<br>", $output) : '-' );
450 break;
451 }
452 case "commercial":
453 {
454 $output = array();
455 if ( isset($result['profile']['locations']) && is_array($result['profile']['locations']) && !empty($result['profile']['locations']) )
456 {
457 $output_locations = array();
458 foreach ( $result['profile']['locations'] as $profile_location )
459 {
460 if ( is_scalar( $profile_location ) && isset( $locations[$profile_location] ) ) {
461 $output_locations[] = $locations[$profile_location];
462 }
463 }
464 $output[] = '<strong>Locations:</strong> ' . esc_html(implode(", ", $output_locations));
465 }
466 if ( isset($result['profile']['notes']) && $result['profile']['notes'] != '' )
467 {
468 $output[] = '<strong>Additional Requirements:</strong> ' . nl2br(esc_html($result['profile']['notes']));
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.
471 echo( !empty($output) ? implode("<br>", $output) : '-' );
472 break;
473 }
474 }
475 }
476 ?></td>
477 </tr>
478 <?php
479 }
480 }
481 else
482 {
483 ?>
484 <tr>
485 <td colspan="3" style="text-align:center"><?php echo esc_html(__( 'No matching applicants found', 'propertyhive' )); ?></td>
486 </tr>
487 <?php
488 }
489 ?>
490 </tbody>
491 </table>
492 </div>
493 <?php
494 }
495 ?>
496
497 </div>
498
499 </div>
500 <script>
501
502 var custom_departments = <?php echo json_encode(ph_get_custom_departments()); ?>;
503 function toggleDepartmentFields()
504 {
505 if (jQuery('#mainform').length > 0)
506 {
507 // There may be multiple forms on the page so treat each one individually
508 jQuery('#mainform').each(function()
509 {
510 var selectedDepartment = "residential-sales"; // TODO: Use default from settings
511
512 var selected = jQuery(this).find('[name=\'department\']');
513
514 jQuery(this).find('.sales-only').hide();
515 jQuery(this).find('.lettings-only').hide();
516 jQuery(this).find('.residential-only').hide();
517 jQuery(this).find('.commercial-only').hide();
518
519 if (selected.length > 0)
520 {
521 selectedDepartment = selected.val();
522
523 // controls won't always be display:block so we should get the
524 // first visible component (that isnt sales/lettings-only) and
525 // use that display
526 var display = 'block';
527
528 if ( selectedDepartment == 'residential-sales' || ( custom_departments[selectedDepartment] && custom_departments[selectedDepartment].based_on == 'residential-sales' ) )
529 {
530 jQuery(this).find('.sales-only').css('display', display);
531 jQuery(this).find('.residential-only').css('display', display);
532 }
533 else if ( selectedDepartment == 'residential-lettings' || ( custom_departments[selectedDepartment] && custom_departments[selectedDepartment].based_on == 'residential-lettings' ) )
534 {
535 jQuery(this).find('.lettings-only').css('display', display);
536 jQuery(this).find('.residential-only').css('display', display);
537 }
538 else if ( selectedDepartment == 'commercial' || ( custom_departments[selectedDepartment] && custom_departments[selectedDepartment].based_on == 'commercial' ) )
539 {
540 jQuery(this).find('.commercial-only').css('display', display);
541 }
542 }
543 });
544 }
545 }
546
547 jQuery( function(jQuery) {
548
549 // Multiselect
550 jQuery("#mainform select.multiselect").chosen();
551
552 toggleDepartmentFields();
553
554 jQuery('#mainform [name=\'department\']').change(function()
555 {
556 toggleDepartmentFields();
557 });
558
559 jQuery('#export_applicant_list_results_button').click(function(e)
560 {
561 e.preventDefault();
562 jQuery('input[name=\'export_applicant_list_results\']').val('1');
563 jQuery('#mainform').submit();
564
565 setTimeout(function() { jQuery('input[name=\'export_applicant_list_results\']').val(''); }, 1000);
566 });
567 });
568
569 jQuery(window).resize(function() {
570 toggleDepartmentFields();
571 });
572
573 </script>
574 <?php
575 }
576
577 public function generate_results()
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
607 $search_property_types = array();
608 if (
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 &&
611 (
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'
620 )
621 )
622 {
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 ) )
625 {
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 )
628 {
629 $search_property_types[] = (int)$property_type;
630
631 $args = array(
632 'hide_empty' => false,
633 'parent' => $property_type
634 );
635 $terms = get_terms( array_merge( wp_parse_args( $args ), array( 'taxonomy' => 'property_type' ) ) );
636
637 if ( !empty( $terms ) && !is_wp_error( $terms ) )
638 {
639 foreach ($terms as $term)
640 {
641 $search_property_types[] = $term->term_id;
642
643 $args = array(
644 'hide_empty' => false,
645 'parent' => $term->term_id
646 );
647 $subterms = get_terms( array_merge( wp_parse_args( $args ), array( 'taxonomy' => 'property_type' ) ) );
648
649 if ( !empty( $subterms ) && !is_wp_error( $subterms ) )
650 {
651 foreach ($subterms as $term)
652 {
653 $search_property_types[] = $term->term_id;
654 }
655 }
656 }
657 }
658 }
659 }
660 $search_property_types = array_unique($search_property_types);
661 }
662
663 $search_locations = array();
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 ) )
666 {
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 )
669 {
670 $search_locations[] = (int)$location;
671
672 $args = array(
673 'hide_empty' => false,
674 'parent' => $location
675 );
676 $terms = get_terms( array_merge( wp_parse_args( $args ), array( 'taxonomy' => 'location' ) ) );
677
678 if ( !empty( $terms ) && !is_wp_error( $terms ) )
679 {
680 foreach ($terms as $term)
681 {
682 $search_locations[] = $term->term_id;
683
684 $args = array(
685 'hide_empty' => false,
686 'parent' => $term->term_id
687 );
688 $subterms = get_terms( array_merge( wp_parse_args( $args ), array( 'taxonomy' => 'location' ) ) );
689
690 if ( !empty( $subterms ) && !is_wp_error( $subterms ) )
691 {
692 foreach ($subterms as $term)
693 {
694 $search_locations[] = $term->term_id;
695 }
696 }
697 }
698 }
699 }
700 }
701 $search_locations = array_unique($search_locations);
702
703 $args = array(
704 'post_type' => 'contact',
705 'fields' => 'ids',
706 'nopaging' => true,
707 );
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.
710 $args['meta_query'] = array();
711
712 $args['meta_query'][] = array(
713 'key' => '_contact_types',
714 'value' => 'applicant',
715 'compare' => 'LIKE'
716 );
717
718 $applicant_query = new WP_Query($args);
719
720 $results = array();
721
722 if ( $applicant_query->have_posts() )
723 {
724 $percentage_lower = get_option( 'propertyhive_applicant_match_price_range_percentage_lower', '' );
725 $percentage_higher = get_option( 'propertyhive_applicant_match_price_range_percentage_higher', '' );
726
727 while ( $applicant_query->have_posts() )
728 {
729 $applicant_query->the_post();
730
731 $num_applicant_profiles = get_post_meta( get_the_ID(), '_applicant_profiles', TRUE );
732 if ( $num_applicant_profiles == '' )
733 {
734 $num_applicant_profiles = 0;
735 }
736
737 for ( $i = 0; $i < $num_applicant_profiles; ++$i )
738 {
739 $profile = get_post_meta( get_the_ID(), '_applicant_profile_' . $i, TRUE );
740
741 $match = true;
742
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 )
745 {
746 if ( !isset($profile['send_matching_properties']) || ( isset($profile['send_matching_properties']) && $profile['send_matching_properties'] != 'yes' ) )
747 {
748 $match = false;
749 }
750 }
751
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 )
754 {
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) )
757 {
758 $match = false;
759 }
760 }
761
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' ) )
764 {
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) != '' )
767 {
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));
770
771 if ( $percentage_lower != '' && $percentage_higher != '' )
772 {
773 $match_price_range_lower = '';
774 if ( !isset($profile['match_price_range_lower_actual']) || ( isset($profile['match_price_range_lower_actual']) && $profile['match_price_range_lower_actual'] == '' ) )
775 {
776 if ( isset($profile['max_price_actual']) && $profile['max_price_actual'] != '' )
777 {
778 if ( $percentage_lower != '' )
779 {
780 $match_price_range_lower = $profile['max_price_actual'] - ( $profile['max_price_actual'] * ( $percentage_lower / 100 ) );
781 }
782 }
783 }
784 else
785 {
786 $match_price_range_lower = $profile['match_price_range_lower_actual'];
787 }
788
789 $match_price_range_higher = '';
790 if ( !isset($profile['match_price_range_higher_actual']) || ( isset($profile['match_price_range_higher_actual']) && $profile['match_price_range_higher_actual'] == '' ) )
791 {
792 if ( isset($profile['max_price_actual']) && $profile['max_price_actual'] != '' )
793 {
794 if ( $percentage_higher != '' )
795 {
796 $match_price_range_higher = $profile['max_price_actual'] + ( $profile['max_price_actual'] * ( $percentage_higher / 100 ) );
797 }
798 }
799 }
800 else
801 {
802 $match_price_range_higher = $profile['match_price_range_higher_actual'];
803 }
804
805 if (
806 !($match_price_range_lower == '' && $match_price_range_higher == '') && // Both bounds are not empty
807 !(
808 $price >= $match_price_range_lower &&
809 $price <= $match_price_range_higher
810 ) // Price is not within the bounds
811 ) {
812 $match = false; // Assuming you want to set match to false; adjust as needed
813 }
814 }
815 else
816 {
817 if (
818 isset($profile['max_price_actual']) &&
819 $profile['max_price_actual'] !== '' && // Checks if max_price_actual is not an empty string
820 $price > $profile['max_price_actual'] // Checks if price is greater than max_price_actual
821 )
822 {
823 $match = false;
824 }
825 }
826 }
827 }
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' ) )
830 {
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) != '' )
833 {
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));
836
837 if ( isset($profile['max_price_actual']) && $profile['max_price_actual'] != '' && $profile['max_price_actual'] != 0 && $profile['max_price_actual'] < $price )
838 {
839 $match = false;
840 }
841 }
842 }
843 if (
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 &&
846 (
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'
855 )
856 )
857 {
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) != '' )
860 {
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));
863
864 if ( isset($profile['min_beds']) && $profile['min_beds'] != '' && $profile['min_beds'] != 0 && $profile['min_beds'] > $beds )
865 {
866 $match = false;
867 }
868 }
869
870 // 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 ) )
873 {
874 $found_type = false;
875 foreach ( $search_property_types as $search_property_type )
876 {
877 if ( isset($profile['property_types']) && is_array($profile['property_types']) && in_array($search_property_type, $profile['property_types']) )
878 {
879 $found_type = true;
880 }
881 }
882
883 if ( !$found_type )
884 {
885 $match = false;
886 }
887 }
888 }
889
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 ) )
892 {
893 $found_type = false;
894 foreach ( $search_locations as $search_location )
895 {
896 if ( isset($profile['locations']) && is_array($profile['locations']) && in_array($search_location, $profile['locations']) )
897 {
898 $found_type = true;
899 }
900 }
901
902 if ( !$found_type )
903 {
904 $match = false;
905 }
906 }
907
908 $match = apply_filters( 'propertyhive_applicant_list_check', $match, get_the_ID(), $profile );
909
910 if ( $match )
911 {
912 $contact = new PH_Contact( get_the_ID() );
913
914 $results[] = array(
915 'contact_id' => get_the_ID(),
916 'applicant_profile_id' => $i,
917 'name' => get_the_title(),
918 'edit_link' => get_edit_post_link(get_the_ID()),
919 'telephone_number' => $contact->_telephone_number,
920 'email_address' => $contact->_email_address,
921 'address' => $contact->get_formatted_full_address(),
922 'profile' => $profile
923 );
924 }
925 }
926 }
927 }
928
929 wp_reset_postdata();
930
931 return $results;
932 }
933
934 private function array_2_csv($results)
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
942 $locations = array();
943 $args = array(
944 'hide_empty' => false,
945 'parent' => 0
946 );
947 $terms = get_terms( array_merge( wp_parse_args( $args ), array( 'taxonomy' => 'location' ) ) );
948
949 if ( !empty( $terms ) && !is_wp_error( $terms ) )
950 {
951 foreach ($terms as $term)
952 {
953 $locations[$term->term_id] = html_entity_decode( $term->name, ENT_QUOTES, get_bloginfo( 'charset' ) );
954
955 $args = array(
956 'hide_empty' => false,
957 'parent' => $term->term_id
958 );
959 $subterms = get_terms( array_merge( wp_parse_args( $args ), array( 'taxonomy' => 'location' ) ) );
960
961 if ( !empty( $subterms ) && !is_wp_error( $subterms ) )
962 {
963 foreach ($subterms as $term)
964 {
965 $locations[$term->term_id] = html_entity_decode( $term->name, ENT_QUOTES, get_bloginfo( 'charset' ) );
966
967 $args = array(
968 'hide_empty' => false,
969 'parent' => $term->term_id
970 );
971 $subsubterms = get_terms( array_merge( wp_parse_args( $args ), array( 'taxonomy' => 'location' ) ) );
972
973 if ( !empty( $subsubterms ) && !is_wp_error( $subsubterms ) )
974 {
975 foreach ($subsubterms as $term)
976 {
977 $locations[$term->term_id] = html_entity_decode( $term->name, ENT_QUOTES, get_bloginfo( 'charset' ) );
978 }
979 }
980 }
981 }
982 }
983 }
984
985 $property_types = array();
986 $args = array(
987 'hide_empty' => false,
988 'parent' => 0
989 );
990 $terms = get_terms( array_merge( wp_parse_args( $args ), array( 'taxonomy' => 'property_type' ) ) );
991
992 if ( !empty( $terms ) && !is_wp_error( $terms ) )
993 {
994 foreach ($terms as $term)
995 {
996 $property_types[$term->term_id] = html_entity_decode( $term->name, ENT_QUOTES, get_bloginfo( 'charset' ) );
997
998 $args = array(
999 'hide_empty' => false,
1000 'parent' => $term->term_id
1001 );
1002 $subterms = get_terms( array_merge( wp_parse_args( $args ), array( 'taxonomy' => 'property_type' ) ) );
1003
1004 if ( !empty( $subterms ) && !is_wp_error( $subterms ) )
1005 {
1006 foreach ($subterms as $term)
1007 {
1008 $property_types[$term->term_id] = html_entity_decode( $term->name, ENT_QUOTES, get_bloginfo( 'charset' ) );
1009 }
1010 }
1011 }
1012 }
1013
1014 $percentage_lower = get_option( 'propertyhive_applicant_match_price_range_percentage_lower', '' );
1015 $percentage_higher = get_option( 'propertyhive_applicant_match_price_range_percentage_higher', '' );
1016
1017 ob_start();
1018
1019 $df = fopen("php://output", 'w');
1020
1021 $columns = array(
1022 'name' => __( 'Name', 'propertyhive' ),
1023 'email_address' => __( 'Email Address', 'propertyhive' ),
1024 'telephone_number' => __( 'Telephone Number', 'propertyhive' ),
1025 'address' => __( 'Address', 'propertyhive' ),
1026 'department' => __( 'Department', 'propertyhive' ),
1027 );
1028
1029 if ( $has_department )
1030 {
1031 $department = ph_clean( $department_input );
1032 if ( ph_get_custom_department_based_on($department) !== FALSE )
1033 {
1034 $department = ph_get_custom_department_based_on($department);
1035 }
1036 }
1037 if ( isset($department) )
1038 {
1039 switch ( $department )
1040 {
1041 case "residential-sales":
1042 {
1043 $columns['maximum_price'] = __( 'Maximum Price', 'propertyhive' );
1044 if ( $percentage_lower != '' && $percentage_higher != '' ) {
1045 $columns['maximum_price_range'] = __( 'Maximum Price Range', 'propertyhive' );
1046 }
1047 break;
1048 }
1049 case "residential-lettings":
1050 {
1051 $columns['maximum_rent'] = __( 'Maximum Rent (PCM)', 'propertyhive' );
1052 break;
1053 }
1054 }
1055 if ( $department == 'residential-sales' || $department == 'residential-lettings' )
1056 {
1057 $columns['currency'] = __( 'Currency', 'propertyhive' );
1058 $columns['minimum_bedrooms'] = __( 'Minimum Bedrooms', 'propertyhive' );
1059 $columns['property_types'] = __( 'Property Types', 'propertyhive' );
1060 }
1061 }
1062
1063 $columns['locations'] = __( 'Locations', 'propertyhive' );
1064 $columns['additional_requirements'] = __( 'Additional Requirements', 'propertyhive' );
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.
1068 $columns = apply_filters( 'propertyhive_export_applicant_list_columns', $columns, $_POST );
1069
1070 fputcsv( $df, $columns, ',', '"', '' );
1071
1072 foreach ($results as $result)
1073 {
1074 $columns = array(
1075 'name' => $result['name'],
1076 'email_address' => $result['email_address'],
1077 'telephone_number' => $result['telephone_number'],
1078 'address' => $result['address'],
1079 'department' => ( isset($result['profile']['department']) ? propertyhive_get_department_label( $result['profile']['department'] ) : '-' ),
1080 );
1081
1082 if ( isset($department) )
1083 {
1084 switch ( $department )
1085 {
1086 case "residential-sales":
1087 {
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 }
1092
1093 if ( !empty($columns['maximum_price']) )
1094 {
1095 if ( $percentage_lower != '' && $percentage_higher != '' )
1096 {
1097 $match_price_range_lower = '';
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'] == '' ) )
1099 {
1100 if ( isset($result['profile']['max_price']) && $result['profile']['max_price'] != '' )
1101 {
1102 $match_price_range_lower = (float) $result['profile']['max_price'] - ( (float) $result['profile']['max_price'] * ( (float) $percentage_lower / 100 ) );
1103 }
1104 }
1105 else
1106 {
1107 $match_price_range_lower = $result['profile']['match_price_range_lower'];
1108 }
1109
1110 $match_price_range_higher = '';
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'] == '' ) )
1112 {
1113 if ( isset($result['profile']['max_price']) && $result['profile']['max_price'] != '' )
1114 {
1115 $match_price_range_higher = (float) $result['profile']['max_price'] + ( (float) $result['profile']['max_price'] * ( (float) $percentage_higher / 100 ) );
1116 }
1117 }
1118 else
1119 {
1120 $match_price_range_higher = $result['profile']['match_price_range_higher'];
1121 }
1122
1123 if (
1124 $match_price_range_lower != '' && $match_price_range_higher != ''
1125 )
1126 {
1127 $columns['maximum_price_range'] = $match_price_range_lower . ' - ' . $match_price_range_higher;
1128 }
1129 }
1130 }
1131
1132 $columns['currency'] = ( isset($result['profile']['currency']) ? $result['profile']['currency'] : 'GBP' );
1133
1134 break;
1135 }
1136 case "residential-lettings":
1137 {
1138 $columns['maximum_rent'] = ( isset($result['profile']['max_rent']) ? $result['profile']['max_rent'] : '' );
1139 $columns['currency'] = ( isset($result['profile']['currency']) ? $result['profile']['currency'] : 'GBP' );
1140 break;
1141 }
1142 }
1143 if ( $department == 'residential-sales' || $department == 'residential-lettings' )
1144 {
1145 $columns['minimum_bedrooms'] = ( isset($result['profile']['min_beds']) ? $result['profile']['min_beds'] : '' );
1146
1147 $output_types = array();
1148 if ( isset($result['profile']['property_types']) && is_array($result['profile']['property_types']) && !empty($result['profile']['property_types']) )
1149 {
1150 foreach ( $result['profile']['property_types'] as $profile_type )
1151 {
1152 if ( is_scalar( $profile_type ) && isset( $property_types[$profile_type] ) ) {
1153 $output_types[] = $property_types[$profile_type];
1154 }
1155 }
1156 }
1157 $columns['property_types'] = implode(", ", $output_types);
1158 }
1159 }
1160
1161 $output_locations = array();
1162 if ( isset($result['profile']['locations']) && is_array($result['profile']['locations']) && !empty($result['profile']['locations']) )
1163 {
1164 foreach ( $result['profile']['locations'] as $profile_location )
1165 {
1166 if ( is_scalar( $profile_location ) && isset( $locations[$profile_location] ) ) {
1167 $output_locations[] = $locations[$profile_location];
1168 }
1169 }
1170 }
1171 $columns['locations'] = implode(", ", $output_locations);
1172
1173 $columns['additional_requirements'] = isset($result['profile']['notes']) ? $result['profile']['notes'] : '';
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.
1177 $columns = apply_filters( 'propertyhive_export_applicant_list_row_data', $columns, $_POST, $result['contact_id'], $result['applicant_profile_id'] );
1178
1179 fputcsv( $df, $columns, ',', '"', '' );
1180 }
1181 fclose($df); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose -- Closes the php://output CSV stream.
1182
1183 return ob_get_clean();
1184 }
1185
1186 public function export()
1187 {
1188 if ( !isset( $_POST['ph_applicant_export_nonce'] ) || ! check_admin_referer( 'ph_applicant_export', 'ph_applicant_export_nonce', false ) )
1189 {
1190 wp_die( esc_html__( 'Invalid request (nonce failure)', 'propertyhive' ), 403 );
1191 }
1192
1193 if ( !current_user_can( 'manage_propertyhive' ) )
1194 {
1195 wp_die( esc_html__( 'Insufficient permissions', 'propertyhive' ), 403 );
1196 }
1197
1198 $filename = 'applicant-list-' . gmdate("YmdHis") . '.csv';
1199
1200 // disable caching
1201 $now = gmdate("D, d M Y H:i:s");
1202 header("Expires: Tue, 03 Jul 2001 06:00:00 GMT");
1203 header("Cache-Control: max-age=0, no-cache, must-revalidate, proxy-revalidate");
1204 header("Last-Modified: {$now} GMT");
1205
1206 // force download
1207 header("Content-Type: application/force-download");
1208 header("Content-Type: application/octet-stream");
1209 header("Content-Type: application/download");
1210
1211 // disposition / encoding on response body
1212 header("Content-Disposition: attachment;filename={$filename}");
1213 header("Content-Transfer-Encoding: binary");
1214
1215 $results = $this->generate_results();
1216
1217 // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Attachment response is CSV encoded by fputcsv, not HTML; HTML escaping would corrupt exported values.
1218 echo $this->array_2_csv($results);
1219
1220 die();
1221 }
1222 }
1223
1224 endif;
1225