PluginProbe
Property Hive / 2.4.0
Property Hive v2.4.0
2.4.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 All 262 releases
← All changes | includes/admin/class-ph-admin-applicant-list.php +852 -263 1.4.46 → 2.4.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,40 +106,31 @@
72 106 </select>
73 107 </p>
74 108
75 109 <p class="form-field sales-only">
76 - <label>Maximum Price From</label>
77 - <input type="text" name="maximum_price_from" value="<?php if ( isset($_POST['maximum_price_from']) ) { echo esc_attr( $_POST['maximum_price_from'] ); } ?>">
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 - <p class="form-field sales-only">
81 - <label>Maximum Price To</label>
82 - <input type="text" name="maximum_price_to" value="<?php if ( isset($_POST['maximum_price_to']) ) { echo esc_attr( $_POST['maximum_price_to'] ); } ?>">
83 - </p>
84 -
85 116 <p class="form-field lettings-only">
86 - <label>Maximum Rent From (PCM)</label>
87 - <input type="text" name="maximum_rent_from" value="<?php if ( isset($_POST['maximum_rent_from']) ) { echo esc_attr( $_POST['maximum_rent_from'] ); } ?>">
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 ); } ?>">
88 121 </p>
89 122
90 - <p class="form-field lettings-only">
91 - <label>Maximum Rent To (PCM)</label>
92 - <input type="text" name="maximum_rent_to" value="<?php if ( isset($_POST['maximum_rent_to']) ) { echo esc_attr( $_POST['maximum_rent_to'] ); } ?>">
93 - </p>
94 -
95 123 <p class="form-field residential-only">
96 - <label>Minimum Bedrooms From</label>
97 - <input type="number" name="minimum_bedrooms_from" class="short" value="<?php if ( isset($_POST['minimum_bedrooms_from']) ) { echo esc_attr( $_POST['minimum_bedrooms_from'] ); } ?>">
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 ); } ?>">
98 128 </p>
99 129
100 130 <p class="form-field residential-only">
101 - <label>Minimum Bedrooms To</label>
102 - <input type="number" name="minimum_bedrooms_to" class="short" value="<?php if ( isset($_POST['minimum_bedrooms_to']) ) { echo esc_attr( $_POST['minimum_bedrooms_to'] ); } ?>">
103 - </p>
104 -
105 - <p class="form-field residential-only">
106 - <label>Property Types</label>
107 - <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">
108 133 <?php
109 134 $options = array( '' => '' );
110 135 $args = array(
111 136 'hide_empty' => false,
@@ -110,18 +135,19 @@
110 135 $args = array(
111 136 'hide_empty' => false,
112 137 'parent' => 0
113 138 );
114 - $terms = get_terms( 'property_type', $args );
139 + $terms = get_terms( array_merge( wp_parse_args( $args ), array( 'taxonomy' => 'property_type' ) ) );
115 140
116 141 if ( !empty( $terms ) && !is_wp_error( $terms ) )
117 142 {
118 143 foreach ($terms as $term)
119 144 {
120 - $property_types[$term->term_id] = esc_html( $term->name );
145 + $property_types[$term->term_id] = $term->name;
121 146
122 147 echo '<option value="' . esc_attr( $term->term_id ) . '"';
123 - 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 ) )
124 150 {
125 151 echo ' selected';
126 152 }
127 153 echo '>' . esc_html( $term->name ) . '</option>';
@@ -129,18 +155,19 @@
129 155 $args = array(
130 156 'hide_empty' => false,
131 157 'parent' => $term->term_id
132 158 );
133 - $subterms = get_terms( 'property_type', $args );
159 + $subterms = get_terms( array_merge( wp_parse_args( $args ), array( 'taxonomy' => 'property_type' ) ) );
134 160
135 161 if ( !empty( $subterms ) && !is_wp_error( $subterms ) )
136 162 {
137 163 foreach ($subterms as $term)
138 164 {
139 - $property_types[$term->term_id] = esc_html( $term->name );
165 + $property_types[$term->term_id] = $term->name;
140 166
141 167 echo '<option value="' . esc_attr( $term->term_id ) . '"';
142 - 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 ) )
143 170 {
144 171 echo ' selected';
145 172 }
146 173 echo '>- ' . esc_html( $term->name ) . '</option>';
@@ -152,10 +179,10 @@
152 179 </select>
153 180 </p>
154 181
155 182 <p class="form-field">
156 - <label>Location</label>
157 - <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">
158 185 <?php
159 186 $options = array( '' => '' );
160 187 $args = array(
161 188 'hide_empty' => false,
@@ -160,18 +187,19 @@
160 187 $args = array(
161 188 'hide_empty' => false,
162 189 'parent' => 0
163 190 );
164 - $terms = get_terms( 'location', $args );
191 + $terms = get_terms( array_merge( wp_parse_args( $args ), array( 'taxonomy' => 'location' ) ) );
165 192
166 193 if ( !empty( $terms ) && !is_wp_error( $terms ) )
167 194 {
168 195 foreach ($terms as $term)
169 196 {
170 - $locations[$term->term_id] = esc_html( $term->name );
197 + $locations[$term->term_id] = $term->name;
171 198
172 199 echo '<option value="' . esc_attr( $term->term_id ) . '"';
173 - 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 ) )
174 202 {
175 203 echo ' selected';
176 204 }
177 205 echo '>' . esc_html( $term->name ) . '</option>';
@@ -179,18 +207,19 @@
179 207 $args = array(
180 208 'hide_empty' => false,
181 209 'parent' => $term->term_id
182 210 );
183 - $subterms = get_terms( 'location', $args );
211 + $subterms = get_terms( array_merge( wp_parse_args( $args ), array( 'taxonomy' => 'location' ) ) );
184 212
185 213 if ( !empty( $subterms ) && !is_wp_error( $subterms ) )
186 214 {
187 215 foreach ($subterms as $term)
188 216 {
189 - $locations[$term->term_id] = esc_html( $term->name );
217 + $locations[$term->term_id] = $term->name;
190 218
191 219 echo '<option value="' . esc_attr( $term->term_id ) . '"';
192 - 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 ) )
193 222 {
194 223 echo ' selected';
195 224 }
196 225 echo '>- ' . esc_html( $term->name ) . '</option>';
@@ -198,18 +227,19 @@
198 227 $args = array(
199 228 'hide_empty' => false,
200 229 'parent' => $term->term_id
201 230 );
202 - $subsubterms = get_terms( 'location', $args );
231 + $subsubterms = get_terms( array_merge( wp_parse_args( $args ), array( 'taxonomy' => 'location' ) ) );
203 232
204 233 if ( !empty( $subsubterms ) && !is_wp_error( $subsubterms ) )
205 234 {
206 235 foreach ($subsubterms as $term)
207 236 {
208 - $locations[$term->term_id] = esc_html( $term->name );
237 + $locations[$term->term_id] = $term->name;
209 238
210 239 echo '<option value="' . esc_attr( $term->term_id ) . '"';
211 - 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 ) )
212 242 {
213 243 echo ' selected';
214 244 }
215 245 echo '>- - ' . esc_html( $term->name ) . '</option>';
@@ -223,11 +253,22 @@
223 253 </select>
224 254 </p>
225 255
226 256 <p class="form-field">
227 - <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'; } ?>>
228 261 </p>
229 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 +
230 271 </div>
231 272
232 273 </form>
233 274
@@ -233,185 +274,22 @@
233 274
234 275 <div class="applicant-list-results">
235 276
236 277 <?php
237 - 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' )
238 280 {
239 - $args = array(
240 - 'post_type' => 'contact',
241 - 'nopaging' => true,
242 - );
243 -
244 - $args['meta_query'] = array();
245 -
246 - $args['meta_query'][] = array(
247 - 'key' => '_contact_types',
248 - 'value' => 'applicant',
249 - 'compare' => 'LIKE'
250 - );
251 -
252 - $applicant_query = new WP_Query($args);
253 -
254 - $results = array();
255 -
256 - if ( $applicant_query->have_posts() )
257 - {
258 - while ( $applicant_query->have_posts() )
259 - {
260 - $applicant_query->the_post();
261 -
262 - $num_applicant_profiles = get_post_meta( get_the_ID(), '_applicant_profiles', TRUE );
263 - if ( $num_applicant_profiles == '' )
264 - {
265 - $num_applicant_profiles = 0;
266 - }
267 -
268 - for ( $i = 0; $i < $num_applicant_profiles; ++$i )
269 - {
270 - $profile = get_post_meta( get_the_ID(), '_applicant_profile_' . $i, TRUE );
271 -
272 - $match = true;
273 -
274 - if ( isset($_POST['department']) )
275 - {
276 - if ( isset($profile['department']) && $profile['department'] != ph_clean($_POST['department']) )
277 - {
278 - $match = false;
279 - }
280 - }
281 -
282 - if ( isset($_POST['department']) && $_POST['department'] == 'residential-sales' )
283 - {
284 - if ( isset($_POST['maximum_price_from']) && ph_clean($_POST['maximum_price_from']) != '' )
285 - {
286 - $price = preg_replace("/[^0-9]/", '', ph_clean($_POST['maximum_price_from']));
287 -
288 - if ( isset($profile['max_price_actual']) && $profile['max_price_actual'] != '' && $profile['max_price_actual'] != 0 && $profile['max_price_actual'] < $price )
289 - {
290 - $match = false;
291 - }
292 - }
293 - if ( isset($_POST['maximum_price_to']) && ph_clean($_POST['maximum_price_to']) != '' )
294 - {
295 - $price = preg_replace("/[^0-9]/", '', ph_clean($_POST['maximum_price_to']));
296 -
297 - if ( isset($profile['max_price_actual']) && $profile['max_price_actual'] != '' && $profile['max_price_actual'] != 0 && $profile['max_price_actual'] > $price )
298 - {
299 - $match = false;
300 - }
301 - }
302 - }
303 - if ( isset($_POST['department']) && $_POST['department'] == 'residential-lettings' )
304 - {
305 - if ( isset($_POST['maximum_rent_from']) && ph_clean($_POST['maximum_rent_from']) != '' )
306 - {
307 - $price = preg_replace("/[^0-9]/", '', ph_clean($_POST['maximum_rent_from']));
308 -
309 - if ( isset($profile['max_rent_actual']) && $profile['max_rent_actual'] != '' && $profile['max_rent_actual'] != 0 && $profile['max_rent_actual'] < $price )
310 - {
311 - $match = false;
312 - }
313 - }
314 - if ( isset($_POST['maximum_rent_to']) && ph_clean($_POST['maximum_rent_to']) != '' )
315 - {
316 - $price = preg_replace("/[^0-9]/", '', ph_clean($_POST['maximum_rent_to']));
317 -
318 - if ( isset($profile['max_rent_actual']) && $profile['max_rent_actual'] != '' && $profile['max_rent_actual'] != 0 && $profile['max_rent_actual'] > $price )
319 - {
320 - $match = false;
321 - }
322 - }
323 - }
324 - if ( isset($_POST['department']) && ( $_POST['department'] == 'residential-sales' || $_POST['department'] == 'residential-lettings' ) )
325 - {
326 - if ( isset($_POST['minimum_bedrooms_from']) && ph_clean($_POST['minimum_bedrooms_from']) != '' )
327 - {
328 - $beds = preg_replace("/[^0-9]/", '', ph_clean($_POST['minimum_bedrooms_from']));
329 -
330 - if ( isset($profile['min_beds']) && $profile['min_beds'] != '' && $profile['min_beds'] != 0 && $profile['min_beds'] < $beds )
331 - {
332 - $match = false;
333 - }
334 - }
335 - if ( isset($_POST['minimum_bedrooms_to']) && ph_clean($_POST['minimum_bedrooms_to']) != '' )
336 - {
337 - $beds = preg_replace("/[^0-9]/", '', ph_clean($_POST['minimum_bedrooms_to']));
338 -
339 - if ( isset($profile['min_beds']) && $profile['min_beds'] != '' && $profile['min_beds'] != 0 && $profile['min_beds'] > $beds )
340 - {
341 - $match = false;
342 - }
343 - }
344 -
345 - // Property Types
346 - if ( isset($_POST['property_types']) && is_array($_POST['property_types']) && !empty($_POST['property_types']) )
347 - {
348 - $found_type = false;
349 - foreach ($_POST['property_types'] as $search_property_type)
350 - {
351 - if ( isset($profile['property_types']) && is_array($profile['property_types']) && in_array($search_property_type, $profile['property_types']) )
352 - {
353 - $found_type = true;
354 - }
355 - }
356 -
357 - if ( !$found_type )
358 - {
359 - $match = false;
360 - }
361 - }
362 - }
363 -
364 - if ( isset($_POST['locations']) && is_array($_POST['locations']) && !empty($_POST['locations']) )
365 - {
366 - $found_type = false;
367 - foreach ($_POST['locations'] as $search_location)
368 - {
369 - if ( isset($profile['locations']) && is_array($profile['locations']) && in_array($search_location, $profile['locations']) )
370 - {
371 - $found_type = true;
372 - }
373 - }
374 -
375 - if ( !$found_type )
376 - {
377 - $match = false;
378 - }
379 - }
380 -
381 - if ( $match )
382 - {
383 - $contact_details = array();
384 - if ( get_post_meta( get_the_ID(), '_telephone_number', TRUE ) != '' )
385 - {
386 - $contact_details[] = 'T: ' . get_post_meta( get_the_ID(), '_telephone_number', TRUE );
387 - }
388 - if ( get_post_meta( get_the_ID(), '_email_address', TRUE ) != '' )
389 - {
390 - $contact_details[] = 'E: ' . get_post_meta( get_the_ID(), '_email_address', TRUE );
391 - }
392 - $results[] = array(
393 - 'name' => get_the_title(),
394 - 'edit_link' => get_edit_post_link(get_the_ID()),
395 - 'contact_details' => implode("<br>", $contact_details),
396 - 'profile' => $profile
397 - );
398 - }
399 - }
400 - }
401 - }
402 -
403 - wp_reset_postdata();
281 + $results = $this->generate_results();
404 282 ?>
405 283 <br>
406 284 <div class="applicant-list-results">
407 - <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>
408 286 <table width="100%" cellpadding="8" cellspacing="0">
409 287 <thead>
410 288 <tr>
411 - <th style="text-align:left;">Applicant Name</th>
412 - <th style="text-align:left;">Contact Details</th>
413 - <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>
414 292 </tr>
415 293 </thead>
416 294 <tbody>
417 295 <?php
@@ -416,15 +294,40 @@
416 294 <tbody>
417 295 <?php
418 296 if ( !empty($results) )
419 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 +
420 301 foreach ( $results as $result )
421 - {
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 + }
422 313 ?>
423 314 <tr>
424 - <td><a href="<?php echo $result['edit_link'] ?>" target="_blank"><?php echo $result['name']; ?></a></td>
425 - <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>
426 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
427 330 if ( isset($result['profile']['department']) )
428 331 {
429 332 switch ( $result['profile']['department'] )
430 333 {
@@ -432,13 +335,49 @@
432 335 {
433 336 $output = array();
434 337 if ( isset($result['profile']['max_price']) && $result['profile']['max_price'] != '' && $result['profile']['max_price'] != 0 )
435 338 {
436 - $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 + }
437 376 }
438 377 if ( isset($result['profile']['min_beds']) && $result['profile']['min_beds'] != '' && $result['profile']['min_beds'] != 0 )
439 378 {
440 - $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'] ));
441 380 }
442 381 if ( isset($result['profile']['property_types']) && is_array($result['profile']['property_types']) && !empty($result['profile']['property_types']) )
443 382 {
444 383 $output_types = array();
@@ -443,11 +382,13 @@
443 382 {
444 383 $output_types = array();
445 384 foreach ( $result['profile']['property_types'] as $profile_type )
446 385 {
447 - $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 + }
448 389 }
449 - $output[] = '<strong>Property Types:</strong> ' . implode(", ", $output_types);
390 + $output[] = '<strong>Property Types:</strong> ' . esc_html(implode(", ", $output_types));
450 391 }
451 392 if ( isset($result['profile']['locations']) && is_array($result['profile']['locations']) && !empty($result['profile']['locations']) )
452 393 {
453 394 $output_locations = array();
@@ -452,16 +393,19 @@
452 393 {
453 394 $output_locations = array();
454 395 foreach ( $result['profile']['locations'] as $profile_location )
455 396 {
456 - $output_locations[] = $locations[$profile_location];
397 + if ( is_scalar( $profile_location ) && isset( $locations[$profile_location] ) ) {
398 + $output_locations[] = $locations[$profile_location];
399 + }
457 400 }
458 - $output[] = '<strong>Locations:</strong> ' . implode(", ", $output_locations);
401 + $output[] = '<strong>Locations:</strong> ' . esc_html(implode(", ", $output_locations));
459 402 }
460 403 if ( isset($result['profile']['notes']) && $result['profile']['notes'] != '' )
461 404 {
462 - $output[] = '<strong>Additional Requirements:</strong> ' . nl2br($result['profile']['notes']);
405 + $output[] = '<strong>Additional Requirements:</strong> ' . nl2br(esc_html($result['profile']['notes']));
463 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.
464 408 echo( !empty($output) ? implode("<br>", $output) : '-' );
465 409 break;
466 410 }
467 411 case "residential-lettings":
@@ -468,13 +412,13 @@
468 412 {
469 413 $output = array();
470 414 if ( isset($result['profile']['max_rent']) && $result['profile']['max_rent'] != '' && $result['profile']['max_rent'] != 0 )
471 415 {
472 - $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']);
473 417 }
474 418 if ( isset($result['profile']['min_beds']) && $result['profile']['min_beds'] != '' && $result['profile']['min_beds'] != 0 )
475 419 {
476 - $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'] ));
477 421 }
478 422 if ( isset($result['profile']['property_types']) && is_array($result['profile']['property_types']) && !empty($result['profile']['property_types']) )
479 423 {
480 424 $output_types = array();
@@ -479,11 +423,13 @@
479 423 {
480 424 $output_types = array();
481 425 foreach ( $result['profile']['property_types'] as $profile_type )
482 426 {
483 - $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 + }
484 430 }
485 - $output[] = '<strong>Property Types:</strong> ' . implode(", ", $output_types);
431 + $output[] = '<strong>Property Types:</strong> ' . esc_html(implode(", ", $output_types));
486 432 }
487 433 if ( isset($result['profile']['locations']) && is_array($result['profile']['locations']) && !empty($result['profile']['locations']) )
488 434 {
489 435 $output_locations = array();
@@ -488,16 +434,19 @@
488 434 {
489 435 $output_locations = array();
490 436 foreach ( $result['profile']['locations'] as $profile_location )
491 437 {
492 - $output_locations[] = $locations[$profile_location];
438 + if ( is_scalar( $profile_location ) && isset( $locations[$profile_location] ) ) {
439 + $output_locations[] = $locations[$profile_location];
440 + }
493 441 }
494 - $output[] = '<strong>Locations:</strong> ' . implode(", ", $output_locations);
442 + $output[] = '<strong>Locations:</strong> ' . esc_html(implode(", ", $output_locations));
495 443 }
496 444 if ( isset($result['profile']['notes']) && $result['profile']['notes'] != '' )
497 445 {
498 - $output[] = '<strong>Additional Requirements:</strong> ' . nl2br($result['profile']['notes']);
446 + $output[] = '<strong>Additional Requirements:</strong> ' . nl2br(esc_html($result['profile']['notes']));
499 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.
500 449 echo( !empty($output) ? implode("<br>", $output) : '-' );
501 450 break;
502 451 }
503 452 case "commercial":
@@ -502,38 +451,24 @@
502 451 }
503 452 case "commercial":
504 453 {
505 454 $output = array();
506 - /*if ( isset($result['profile']['max_price']) && $result['profile']['max_price'] != '' && $result['profile']['max_price'] != 0 )
507 - {
508 - $output[] = '<strong>Max Price:</strong> &pound;' . number_format($result['profile']['max_price']);
509 - }
510 - if ( isset($result['profile']['min_beds']) && $result['profile']['min_beds'] != '' && $result['profile']['min_beds'] != 0 )
511 - {
512 - $output[] = '<strong>Min Beds:</strong> ' . number_format($result['profile']['min_beds']);
513 - }
514 - if ( isset($result['profile']['property_types']) && is_array($result['profile']['property_types']) && !empty($result['profile']['property_types']) )
515 - {
516 - $output_types = array();
517 - foreach ( $result['profile']['property_types'] as $profile_type )
518 - {
519 - $output_types[] = $property_types[$profile_type];
520 - }
521 - $output[] = '<strong>Property Types:</strong> ' . implode(", ", $output_types);
522 - }*/
523 455 if ( isset($result['profile']['locations']) && is_array($result['profile']['locations']) && !empty($result['profile']['locations']) )
524 456 {
525 457 $output_locations = array();
526 458 foreach ( $result['profile']['locations'] as $profile_location )
527 459 {
528 - $output_locations[] = $locations[$profile_location];
460 + if ( is_scalar( $profile_location ) && isset( $locations[$profile_location] ) ) {
461 + $output_locations[] = $locations[$profile_location];
462 + }
529 463 }
530 - $output[] = '<strong>Locations:</strong> ' . implode(", ", $output_locations);
464 + $output[] = '<strong>Locations:</strong> ' . esc_html(implode(", ", $output_locations));
531 465 }
532 466 if ( isset($result['profile']['notes']) && $result['profile']['notes'] != '' )
533 467 {
534 - $output[] = '<strong>Additional Requirements:</strong> ' . nl2br($result['profile']['notes']);
468 + $output[] = '<strong>Additional Requirements:</strong> ' . nl2br(esc_html($result['profile']['notes']));
535 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.
536 471 echo( !empty($output) ? implode("<br>", $output) : '-' );
537 472 break;
538 473 }
539 474 }
@@ -546,9 +481,9 @@
546 481 else
547 482 {
548 483 ?>
549 484 <tr>
550 - <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>
551 486 </tr>
552 487 <?php
553 488 }
554 489 ?>
@@ -563,8 +498,9 @@
563 498
564 499 </div>
565 500 <script>
566 501
502 +var custom_departments = <?php echo json_encode(ph_get_custom_departments()); ?>;
567 503 function toggleDepartmentFields()
568 504 {
569 505 if (jQuery('#mainform').length > 0)
570 506 {
@@ -588,19 +524,19 @@
588 524 // first visible component (that isnt sales/lettings-only) and
589 525 // use that display
590 526 var display = 'block';
591 527
592 - if (selectedDepartment == 'residential-sales')
528 + if ( selectedDepartment == 'residential-sales' || ( custom_departments[selectedDepartment] && custom_departments[selectedDepartment].based_on == 'residential-sales' ) )
593 529 {
594 530 jQuery(this).find('.sales-only').css('display', display);
595 531 jQuery(this).find('.residential-only').css('display', display);
596 532 }
597 - else if (selectedDepartment == 'residential-lettings')
533 + else if ( selectedDepartment == 'residential-lettings' || ( custom_departments[selectedDepartment] && custom_departments[selectedDepartment].based_on == 'residential-lettings' ) )
598 534 {
599 535 jQuery(this).find('.lettings-only').css('display', display);
600 536 jQuery(this).find('.residential-only').css('display', display);
601 537 }
602 - else if (selectedDepartment == 'commercial')
538 + else if ( selectedDepartment == 'commercial' || ( custom_departments[selectedDepartment] && custom_departments[selectedDepartment].based_on == 'commercial' ) )
603 539 {
604 540 jQuery(this).find('.commercial-only').css('display', display);
605 541 }
606 542 }
@@ -619,8 +555,16 @@
619 555 {
620 556 toggleDepartmentFields();
621 557 });
622 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 + });
623 567 });
624 568
625 569 jQuery(window).resize(function() {
626 570 toggleDepartmentFields();
@@ -629,7 +573,652 @@
629 573 </script>
630 574 <?php
631 575 }
632 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 + }
633 1222 }
634 1223
635 -endif;
1224 +endif;