PluginProbe
Property Hive / 2.3.0
Property Hive v2.3.0
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 +852 -306 1.4.552.3.0 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.
@@ -22,11 +26,38 @@
22 26 *
23 27 * @access public
24 28 * @return void
25 29 */
26 - public static function output() {
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
@@ -31,16 +62,17 @@
31 62 <div class="wrap propertyhive">
32 63
33 64 <h1>Generate Applicant List</h1>
34 65
35 - <form method="post" id="mainform" action="" enctype="multipart/form-data" class="applicant-list-form">
66 + <form method="post" id="mainform" action="" class="applicant-list-form">
36 67
37 - <input type="hidden" name="submitted" value="1">
68 + <input type="hidden" name="submitted_applicant_list" value="1">
69 + <?php wp_nonce_field( 'ph_applicant_export', 'ph_applicant_export_nonce' ); ?>
38 70
39 71 <div id="poststuff" class="propertyhive_meta_box">
40 72
41 73 <p class="form-field">
42 - <label>Looking For</label>
74 + <label><?php echo esc_html__( 'Looking For', 'propertyhive' ); ?></label>
43 75 <select name="department">
44 76 <?php
45 77
46 78 $departments = ph_get_departments();
@@ -55,18 +87,20 @@
55 87 }
56 88
57 89 foreach ( $department_options as $key => $department )
58 90 {
59 - echo '<option value="' . $key . '"';
60 - if ( isset($_POST['department']) && $_POST['department'] == $key )
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 )
61 94 {
62 95 echo ' selected';
63 96 }
64 - 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' ) )
65 99 {
66 100 echo ' selected';
67 101 }
68 - echo '>' . $department . '</option>';
102 + echo '>' . esc_html($department) . '</option>';
69 103 }
70 104 ?>
71 105
72 106 </select>
@@ -72,25 +106,31 @@
72 106 </select>
73 107 </p>
74 108
75 109 <p class="form-field sales-only">
76 - <label>Price</label>
77 - <input type="text" name="maximum_price" value="<?php if ( isset($_POST['maximum_price']) ) { echo esc_attr( $_POST['maximum_price'] ); } ?>">
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 ); } ?>">
78 114 </p>
79 115
80 116 <p class="form-field lettings-only">
81 - <label>Rent (PCM)</label>
82 - <input type="text" name="maximum_rent" value="<?php if ( isset($_POST['maximum_rent']) ) { echo esc_attr( $_POST['maximum_rent'] ); } ?>">
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 ); } ?>">
83 121 </p>
84 122
85 123 <p class="form-field residential-only">
86 - <label>Bedrooms</label>
87 - <input type="number" name="minimum_bedrooms" class="short" value="<?php if ( isset($_POST['minimum_bedrooms']) ) { echo esc_attr( $_POST['minimum_bedrooms'] ); } ?>">
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 ); } ?>">
88 128 </p>
89 129
90 130 <p class="form-field residential-only">
91 - <label>Property Types</label>
92 - <select id="property_types" name="property_types[]" multiple="multiple" data-placeholder="Start typing to add property types..." class="multiselect attribute_values">
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">
93 133 <?php
94 134 $options = array( '' => '' );
95 135 $args = array(
96 136 'hide_empty' => false,
@@ -95,18 +135,19 @@
95 135 $args = array(
96 136 'hide_empty' => false,
97 137 'parent' => 0
98 138 );
99 - $terms = get_terms( 'property_type', $args );
139 + $terms = get_terms( array_merge( wp_parse_args( $args ), array( 'taxonomy' => 'property_type' ) ) );
100 140
101 141 if ( !empty( $terms ) && !is_wp_error( $terms ) )
102 142 {
103 143 foreach ($terms as $term)
104 144 {
105 - $property_types[$term->term_id] = esc_html( $term->name );
145 + $property_types[$term->term_id] = $term->name;
106 146
107 147 echo '<option value="' . esc_attr( $term->term_id ) . '"';
108 - 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 ) )
109 150 {
110 151 echo ' selected';
111 152 }
112 153 echo '>' . esc_html( $term->name ) . '</option>';
@@ -114,18 +155,19 @@
114 155 $args = array(
115 156 'hide_empty' => false,
116 157 'parent' => $term->term_id
117 158 );
118 - $subterms = get_terms( 'property_type', $args );
159 + $subterms = get_terms( array_merge( wp_parse_args( $args ), array( 'taxonomy' => 'property_type' ) ) );
119 160
120 161 if ( !empty( $subterms ) && !is_wp_error( $subterms ) )
121 162 {
122 163 foreach ($subterms as $term)
123 164 {
124 - $property_types[$term->term_id] = esc_html( $term->name );
165 + $property_types[$term->term_id] = $term->name;
125 166
126 167 echo '<option value="' . esc_attr( $term->term_id ) . '"';
127 - 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 ) )
128 170 {
129 171 echo ' selected';
130 172 }
131 173 echo '>- ' . esc_html( $term->name ) . '</option>';
@@ -137,10 +179,10 @@
137 179 </select>
138 180 </p>
139 181
140 182 <p class="form-field">
141 - <label>Location</label>
142 - <select id="locations" name="locations[]" multiple="multiple" data-placeholder="Start typing to add locations..." class="multiselect attribute_values">
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">
143 185 <?php
144 186 $options = array( '' => '' );
145 187 $args = array(
146 188 'hide_empty' => false,
@@ -145,18 +187,19 @@
145 187 $args = array(
146 188 'hide_empty' => false,
147 189 'parent' => 0
148 190 );
149 - $terms = get_terms( 'location', $args );
191 + $terms = get_terms( array_merge( wp_parse_args( $args ), array( 'taxonomy' => 'location' ) ) );
150 192
151 193 if ( !empty( $terms ) && !is_wp_error( $terms ) )
152 194 {
153 195 foreach ($terms as $term)
154 196 {
155 - $locations[$term->term_id] = esc_html( $term->name );
197 + $locations[$term->term_id] = $term->name;
156 198
157 199 echo '<option value="' . esc_attr( $term->term_id ) . '"';
158 - 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 ) )
159 202 {
160 203 echo ' selected';
161 204 }
162 205 echo '>' . esc_html( $term->name ) . '</option>';
@@ -164,18 +207,19 @@
164 207 $args = array(
165 208 'hide_empty' => false,
166 209 'parent' => $term->term_id
167 210 );
168 - $subterms = get_terms( 'location', $args );
211 + $subterms = get_terms( array_merge( wp_parse_args( $args ), array( 'taxonomy' => 'location' ) ) );
169 212
170 213 if ( !empty( $subterms ) && !is_wp_error( $subterms ) )
171 214 {
172 215 foreach ($subterms as $term)
173 216 {
174 - $locations[$term->term_id] = esc_html( $term->name );
217 + $locations[$term->term_id] = $term->name;
175 218
176 219 echo '<option value="' . esc_attr( $term->term_id ) . '"';
177 - 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 ) )
178 222 {
179 223 echo ' selected';
180 224 }
181 225 echo '>- ' . esc_html( $term->name ) . '</option>';
@@ -183,18 +227,19 @@
183 227 $args = array(
184 228 'hide_empty' => false,
185 229 'parent' => $term->term_id
186 230 );
187 - $subsubterms = get_terms( 'location', $args );
231 + $subsubterms = get_terms( array_merge( wp_parse_args( $args ), array( 'taxonomy' => 'location' ) ) );
188 232
189 233 if ( !empty( $subsubterms ) && !is_wp_error( $subsubterms ) )
190 234 {
191 235 foreach ($subsubterms as $term)
192 236 {
193 - $locations[$term->term_id] = esc_html( $term->name );
237 + $locations[$term->term_id] = $term->name;
194 238
195 239 echo '<option value="' . esc_attr( $term->term_id ) . '"';
196 - 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 ) )
197 242 {
198 243 echo ' selected';
199 244 }
200 245 echo '>- - ' . esc_html( $term->name ) . '</option>';
@@ -208,11 +253,22 @@
208 253 </select>
209 254 </p>
210 255
211 256 <p class="form-field">
212 - <input type="submit" value="<?php echo __( 'Generate Applicant List', 'propertyhive' ); ?>" class="button-primary">
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'; } ?>>
213 261 </p>
214 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 +
215 271 </div>
216 272
217 273 </form>
218 274
@@ -218,243 +274,22 @@
218 274
219 275 <div class="applicant-list-results">
220 276
221 277 <?php
222 - if ( isset($_POST['submitted']) && $_POST['submitted'] == '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' )
223 280 {
224 - $search_property_types = array();
225 - if ( isset($_POST['department']) && ( $_POST['department'] == 'residential-sales' || $_POST['department'] == 'residential-lettings' ) )
226 - {
227 - if ( isset($_POST['property_types']) && is_array($_POST['property_types']) && !empty($_POST['property_types']) )
228 - {
229 - foreach ( $_POST['property_types'] as $property_type )
230 - {
231 - $search_property_types[] = (int)$property_type;
232 -
233 - $args = array(
234 - 'hide_empty' => false,
235 - 'parent' => $property_type
236 - );
237 - $terms = get_terms( 'property_type', $args );
238 -
239 - if ( !empty( $terms ) && !is_wp_error( $terms ) )
240 - {
241 - foreach ($terms as $term)
242 - {
243 - $search_property_types[] = $term->term_id;
244 -
245 - $args = array(
246 - 'hide_empty' => false,
247 - 'parent' => $term->term_id
248 - );
249 - $subterms = get_terms( 'property_type', $args );
250 -
251 - if ( !empty( $subterms ) && !is_wp_error( $subterms ) )
252 - {
253 - foreach ($subterms as $term)
254 - {
255 - $search_property_types[] = $term->term_id;
256 - }
257 - }
258 - }
259 - }
260 - }
261 - }
262 - $search_property_types = array_unique($search_property_types);
263 - }
264 -
265 - $search_locations = array();
266 - if ( isset($_POST['locations']) && is_array($_POST['locations']) && !empty($_POST['locations']) )
267 - {
268 - foreach ( $_POST['locations'] as $location )
269 - {
270 - $search_locations[] = (int)$location;
271 -
272 - $args = array(
273 - 'hide_empty' => false,
274 - 'parent' => $location
275 - );
276 - $terms = get_terms( 'location', $args );
277 -
278 - if ( !empty( $terms ) && !is_wp_error( $terms ) )
279 - {
280 - foreach ($terms as $term)
281 - {
282 - $search_locations[] = $term->term_id;
283 -
284 - $args = array(
285 - 'hide_empty' => false,
286 - 'parent' => $term->term_id
287 - );
288 - $subterms = get_terms( 'location', $args );
289 -
290 - if ( !empty( $subterms ) && !is_wp_error( $subterms ) )
291 - {
292 - foreach ($subterms as $term)
293 - {
294 - $search_locations[] = $term->term_id;
295 - }
296 - }
297 - }
298 - }
299 - }
300 - }
301 - $search_locations = array_unique($search_locations);
302 -
303 - $args = array(
304 - 'post_type' => 'contact',
305 - 'fields' => 'ids',
306 - 'nopaging' => true,
307 - );
308 -
309 - $args['meta_query'] = array();
310 -
311 - $args['meta_query'][] = array(
312 - 'key' => '_contact_types',
313 - 'value' => 'applicant',
314 - 'compare' => 'LIKE'
315 - );
316 -
317 - $applicant_query = new WP_Query($args);
318 -
319 - $results = array();
320 -
321 - if ( $applicant_query->have_posts() )
322 - {
323 - while ( $applicant_query->have_posts() )
324 - {
325 - $applicant_query->the_post();
326 -
327 - $num_applicant_profiles = get_post_meta( get_the_ID(), '_applicant_profiles', TRUE );
328 - if ( $num_applicant_profiles == '' )
329 - {
330 - $num_applicant_profiles = 0;
331 - }
332 -
333 - for ( $i = 0; $i < $num_applicant_profiles; ++$i )
334 - {
335 - $profile = get_post_meta( get_the_ID(), '_applicant_profile_' . $i, TRUE );
336 -
337 - $match = true;
338 -
339 - if ( !isset($profile['send_matching_properties']) || ( isset($profile['send_matching_properties']) && $profile['send_matching_properties'] != 'yes' ) )
340 - {
341 - $match = false;
342 - }
343 -
344 - if ( isset($_POST['department']) )
345 - {
346 - if ( isset($profile['department']) && $profile['department'] != ph_clean($_POST['department']) )
347 - {
348 - $match = false;
349 - }
350 - }
351 -
352 - if ( isset($_POST['department']) && $_POST['department'] == 'residential-sales' )
353 - {
354 - if ( isset($_POST['maximum_price']) && ph_clean($_POST['maximum_price']) != '' )
355 - {
356 - $price = preg_replace("/[^0-9]/", '', ph_clean($_POST['maximum_price']));
357 -
358 - if ( isset($profile['max_price_actual']) && $profile['max_price_actual'] != '' && $profile['max_price_actual'] != 0 && $profile['max_price_actual'] < $price )
359 - {
360 - $match = false;
361 - }
362 - }
363 - }
364 - if ( isset($_POST['department']) && $_POST['department'] == 'residential-lettings' )
365 - {
366 - if ( isset($_POST['maximum_rent']) && ph_clean($_POST['maximum_rent']) != '' )
367 - {
368 - $price = preg_replace("/[^0-9]/", '', ph_clean($_POST['maximum_rent']));
369 -
370 - if ( isset($profile['max_rent_actual']) && $profile['max_rent_actual'] != '' && $profile['max_rent_actual'] != 0 && $profile['max_rent_actual'] < $price )
371 - {
372 - $match = false;
373 - }
374 - }
375 - }
376 - if ( isset($_POST['department']) && ( $_POST['department'] == 'residential-sales' || $_POST['department'] == 'residential-lettings' ) )
377 - {
378 - if ( isset($_POST['minimum_bedrooms']) && ph_clean($_POST['minimum_bedrooms']) != '' )
379 - {
380 - $beds = preg_replace("/[^0-9]/", '', ph_clean($_POST['minimum_bedrooms']));
381 -
382 - if ( isset($profile['min_beds']) && $profile['min_beds'] != '' && $profile['min_beds'] != 0 && $profile['min_beds'] > $beds )
383 - {
384 - $match = false;
385 - }
386 - }
387 -
388 - // Property Types
389 - if ( isset($_POST['property_types']) && is_array($_POST['property_types']) && !empty($_POST['property_types']) )
390 - {
391 - $found_type = false;
392 - foreach ( $search_property_types as $search_property_type )
393 - {
394 - if ( isset($profile['property_types']) && is_array($profile['property_types']) && in_array($search_property_type, $profile['property_types']) )
395 - {
396 - $found_type = true;
397 - }
398 - }
399 -
400 - if ( !$found_type )
401 - {
402 - $match = false;
403 - }
404 - }
405 - }
406 -
407 - if ( isset($_POST['locations']) && is_array($_POST['locations']) && !empty($_POST['locations']) )
408 - {
409 - $found_type = false;
410 - foreach ( $search_locations as $search_location )
411 - {
412 - if ( isset($profile['locations']) && is_array($profile['locations']) && in_array($search_location, $profile['locations']) )
413 - {
414 - $found_type = true;
415 - }
416 - }
417 -
418 - if ( !$found_type )
419 - {
420 - $match = false;
421 - }
422 - }
423 -
424 - if ( $match )
425 - {
426 - $contact_details = array();
427 - if ( get_post_meta( get_the_ID(), '_telephone_number', TRUE ) != '' )
428 - {
429 - $contact_details[] = 'T: ' . get_post_meta( get_the_ID(), '_telephone_number', TRUE );
430 - }
431 - if ( get_post_meta( get_the_ID(), '_email_address', TRUE ) != '' )
432 - {
433 - $contact_details[] = 'E: ' . get_post_meta( get_the_ID(), '_email_address', TRUE );
434 - }
435 - $results[] = array(
436 - 'name' => get_the_title(),
437 - 'edit_link' => get_edit_post_link(get_the_ID()),
438 - 'contact_details' => implode("<br>", $contact_details),
439 - 'profile' => $profile
440 - );
441 - }
442 - }
443 - }
444 - }
445 -
446 - wp_reset_postdata();
281 + $results = $this->generate_results();
447 282 ?>
448 283 <br>
449 284 <div class="applicant-list-results">
450 - <h3><?php echo number_format(count($results)); ?> Applicants Found Matching Your Criteria</h3>
285 + <h3><?php echo esc_html(number_format(count($results))); ?> Applicants Found Matching Your Criteria</h3>
451 286 <table width="100%" cellpadding="8" cellspacing="0">
452 287 <thead>
453 288 <tr>
454 - <th style="text-align:left;">Applicant Name</th>
455 - <th style="text-align:left;">Contact Details</th>
456 - <th style="text-align:left;">Requirements</th>
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>
457 292 </tr>
458 293 </thead>
459 294 <tbody>
460 295 <?php
@@ -459,15 +294,40 @@
459 294 <tbody>
460 295 <?php
461 296 if ( !empty($results) )
462 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 +
463 301 foreach ( $results as $result )
464 - {
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 + }
465 313 ?>
466 314 <tr>
467 - <td><a href="<?php echo $result['edit_link'] ?>" target="_blank"><?php echo $result['name']; ?></a></td>
468 - <td><?php echo ( ( $result['contact_details'] != '' ) ? $result['contact_details'] : '-' ); ?></td>
315 + <td><a href="<?php echo esc_url($result['edit_link']); ?>" target="_blank"><?php echo esc_html($result['name']); ?></a></td>
469 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
470 330 if ( isset($result['profile']['department']) )
471 331 {
472 332 switch ( $result['profile']['department'] )
473 333 {
@@ -475,13 +335,49 @@
475 335 {
476 336 $output = array();
477 337 if ( isset($result['profile']['max_price']) && $result['profile']['max_price'] != '' && $result['profile']['max_price'] != 0 )
478 338 {
479 - $output[] = '<strong>Max Price:</strong> &pound;' . number_format($result['profile']['max_price']);
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 + }
480 376 }
481 377 if ( isset($result['profile']['min_beds']) && $result['profile']['min_beds'] != '' && $result['profile']['min_beds'] != 0 )
482 378 {
483 - $output[] = '<strong>Min Beds:</strong> ' . number_format($result['profile']['min_beds']);
379 + $output[] = '<strong>Min Beds:</strong> ' . esc_html(number_format( (float) $result['profile']['min_beds'] ));
484 380 }
485 381 if ( isset($result['profile']['property_types']) && is_array($result['profile']['property_types']) && !empty($result['profile']['property_types']) )
486 382 {
487 383 $output_types = array();
@@ -486,11 +382,13 @@
486 382 {
487 383 $output_types = array();
488 384 foreach ( $result['profile']['property_types'] as $profile_type )
489 385 {
490 - $output_types[] = $property_types[$profile_type];
386 + if ( is_scalar( $profile_type ) && isset( $property_types[$profile_type] ) ) {
387 + $output_types[] = $property_types[$profile_type];
388 + }
491 389 }
492 - $output[] = '<strong>Property Types:</strong> ' . implode(", ", $output_types);
390 + $output[] = '<strong>Property Types:</strong> ' . esc_html(implode(", ", $output_types));
493 391 }
494 392 if ( isset($result['profile']['locations']) && is_array($result['profile']['locations']) && !empty($result['profile']['locations']) )
495 393 {
496 394 $output_locations = array();
@@ -495,16 +393,19 @@
495 393 {
496 394 $output_locations = array();
497 395 foreach ( $result['profile']['locations'] as $profile_location )
498 396 {
499 - $output_locations[] = $locations[$profile_location];
397 + if ( is_scalar( $profile_location ) && isset( $locations[$profile_location] ) ) {
398 + $output_locations[] = $locations[$profile_location];
399 + }
500 400 }
501 - $output[] = '<strong>Locations:</strong> ' . implode(", ", $output_locations);
401 + $output[] = '<strong>Locations:</strong> ' . esc_html(implode(", ", $output_locations));
502 402 }
503 403 if ( isset($result['profile']['notes']) && $result['profile']['notes'] != '' )
504 404 {
505 - $output[] = '<strong>Additional Requirements:</strong> ' . nl2br($result['profile']['notes']);
405 + $output[] = '<strong>Additional Requirements:</strong> ' . nl2br(esc_html($result['profile']['notes']));
506 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.
507 408 echo( !empty($output) ? implode("<br>", $output) : '-' );
508 409 break;
509 410 }
510 411 case "residential-lettings":
@@ -511,13 +412,13 @@
511 412 {
512 413 $output = array();
513 414 if ( isset($result['profile']['max_rent']) && $result['profile']['max_rent'] != '' && $result['profile']['max_rent'] != 0 )
514 415 {
515 - $output[] = '<strong>Max Rent:</strong> &pound;' . number_format($result['profile']['max_rent']) . $result['profile']['rent_frequency'];
416 + $output[] = '<strong>Max Rent:</strong> ' . $currency . esc_html(ph_display_price_field($result['profile']['max_rent']) . $result['profile']['rent_frequency']);
516 417 }
517 418 if ( isset($result['profile']['min_beds']) && $result['profile']['min_beds'] != '' && $result['profile']['min_beds'] != 0 )
518 419 {
519 - $output[] = '<strong>Min Beds:</strong> ' . number_format($result['profile']['min_beds']);
420 + $output[] = '<strong>Min Beds:</strong> ' . esc_html(number_format( (float) $result['profile']['min_beds'] ));
520 421 }
521 422 if ( isset($result['profile']['property_types']) && is_array($result['profile']['property_types']) && !empty($result['profile']['property_types']) )
522 423 {
523 424 $output_types = array();
@@ -522,11 +423,13 @@
522 423 {
523 424 $output_types = array();
524 425 foreach ( $result['profile']['property_types'] as $profile_type )
525 426 {
526 - $output_types[] = $property_types[$profile_type];
427 + if ( is_scalar( $profile_type ) && isset( $property_types[$profile_type] ) ) {
428 + $output_types[] = $property_types[$profile_type];
429 + }
527 430 }
528 - $output[] = '<strong>Property Types:</strong> ' . implode(", ", $output_types);
431 + $output[] = '<strong>Property Types:</strong> ' . esc_html(implode(", ", $output_types));
529 432 }
530 433 if ( isset($result['profile']['locations']) && is_array($result['profile']['locations']) && !empty($result['profile']['locations']) )
531 434 {
532 435 $output_locations = array();
@@ -531,16 +434,19 @@
531 434 {
532 435 $output_locations = array();
533 436 foreach ( $result['profile']['locations'] as $profile_location )
534 437 {
535 - $output_locations[] = $locations[$profile_location];
438 + if ( is_scalar( $profile_location ) && isset( $locations[$profile_location] ) ) {
439 + $output_locations[] = $locations[$profile_location];
440 + }
536 441 }
537 - $output[] = '<strong>Locations:</strong> ' . implode(", ", $output_locations);
442 + $output[] = '<strong>Locations:</strong> ' . esc_html(implode(", ", $output_locations));
538 443 }
539 444 if ( isset($result['profile']['notes']) && $result['profile']['notes'] != '' )
540 445 {
541 - $output[] = '<strong>Additional Requirements:</strong> ' . nl2br($result['profile']['notes']);
446 + $output[] = '<strong>Additional Requirements:</strong> ' . nl2br(esc_html($result['profile']['notes']));
542 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.
543 449 echo( !empty($output) ? implode("<br>", $output) : '-' );
544 450 break;
545 451 }
546 452 case "commercial":
@@ -545,38 +451,24 @@
545 451 }
546 452 case "commercial":
547 453 {
548 454 $output = array();
549 - /*if ( isset($result['profile']['max_price']) && $result['profile']['max_price'] != '' && $result['profile']['max_price'] != 0 )
550 - {
551 - $output[] = '<strong>Max Price:</strong> &pound;' . number_format($result['profile']['max_price']);
552 - }
553 - if ( isset($result['profile']['min_beds']) && $result['profile']['min_beds'] != '' && $result['profile']['min_beds'] != 0 )
554 - {
555 - $output[] = '<strong>Min Beds:</strong> ' . number_format($result['profile']['min_beds']);
556 - }
557 - if ( isset($result['profile']['property_types']) && is_array($result['profile']['property_types']) && !empty($result['profile']['property_types']) )
558 - {
559 - $output_types = array();
560 - foreach ( $result['profile']['property_types'] as $profile_type )
561 - {
562 - $output_types[] = $property_types[$profile_type];
563 - }
564 - $output[] = '<strong>Property Types:</strong> ' . implode(", ", $output_types);
565 - }*/
566 455 if ( isset($result['profile']['locations']) && is_array($result['profile']['locations']) && !empty($result['profile']['locations']) )
567 456 {
568 457 $output_locations = array();
569 458 foreach ( $result['profile']['locations'] as $profile_location )
570 459 {
571 - $output_locations[] = $locations[$profile_location];
460 + if ( is_scalar( $profile_location ) && isset( $locations[$profile_location] ) ) {
461 + $output_locations[] = $locations[$profile_location];
462 + }
572 463 }
573 - $output[] = '<strong>Locations:</strong> ' . implode(", ", $output_locations);
464 + $output[] = '<strong>Locations:</strong> ' . esc_html(implode(", ", $output_locations));
574 465 }
575 466 if ( isset($result['profile']['notes']) && $result['profile']['notes'] != '' )
576 467 {
577 - $output[] = '<strong>Additional Requirements:</strong> ' . nl2br($result['profile']['notes']);
468 + $output[] = '<strong>Additional Requirements:</strong> ' . nl2br(esc_html($result['profile']['notes']));
578 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.
579 471 echo( !empty($output) ? implode("<br>", $output) : '-' );
580 472 break;
581 473 }
582 474 }
@@ -589,9 +481,9 @@
589 481 else
590 482 {
591 483 ?>
592 484 <tr>
593 - <td colspan="3" style="text-align:center"><?php echo __( 'No matching applicants found', 'propertyhive' ); ?></td>
485 + <td colspan="3" style="text-align:center"><?php echo esc_html(__( 'No matching applicants found', 'propertyhive' )); ?></td>
594 486 </tr>
595 487 <?php
596 488 }
597 489 ?>
@@ -606,8 +498,9 @@
606 498
607 499 </div>
608 500 <script>
609 501
502 +var custom_departments = <?php echo json_encode(ph_get_custom_departments()); ?>;
610 503 function toggleDepartmentFields()
611 504 {
612 505 if (jQuery('#mainform').length > 0)
613 506 {
@@ -631,19 +524,19 @@
631 524 // first visible component (that isnt sales/lettings-only) and
632 525 // use that display
633 526 var display = 'block';
634 527
635 - if (selectedDepartment == 'residential-sales')
528 + if ( selectedDepartment == 'residential-sales' || ( custom_departments[selectedDepartment] && custom_departments[selectedDepartment].based_on == 'residential-sales' ) )
636 529 {
637 530 jQuery(this).find('.sales-only').css('display', display);
638 531 jQuery(this).find('.residential-only').css('display', display);
639 532 }
640 - else if (selectedDepartment == 'residential-lettings')
533 + else if ( selectedDepartment == 'residential-lettings' || ( custom_departments[selectedDepartment] && custom_departments[selectedDepartment].based_on == 'residential-lettings' ) )
641 534 {
642 535 jQuery(this).find('.lettings-only').css('display', display);
643 536 jQuery(this).find('.residential-only').css('display', display);
644 537 }
645 - else if (selectedDepartment == 'commercial')
538 + else if ( selectedDepartment == 'commercial' || ( custom_departments[selectedDepartment] && custom_departments[selectedDepartment].based_on == 'commercial' ) )
646 539 {
647 540 jQuery(this).find('.commercial-only').css('display', display);
648 541 }
649 542 }
@@ -662,8 +555,16 @@
662 555 {
663 556 toggleDepartmentFields();
664 557 });
665 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 + });
666 567 });
667 568
668 569 jQuery(window).resize(function() {
669 570 toggleDepartmentFields();
@@ -672,7 +573,652 @@
672 573 </script>
673 574 <?php
674 575 }
675 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 + }
676 1222 }
677 1223
678 -endif;
1224 +endif;