PluginProbe
Property Hive / 2.3.1
Property Hive v2.3.1
2.3.1 2.3.0 2.2.6 2.2.5 2.2.4 2.2.3 2.2.2 1.4.46 1.4.47 1.4.48 1.4.49 1.4.5 1.4.50 1.4.51 1.4.52 1.4.53 1.4.54 1.4.55 1.4.56 1.4.57 1.4.58 1.4.59 1.4.6 1.4.60 1.4.61 All 261 releases
← All changes | includes/admin/class-ph-admin-post-types.php +1245 -234 1.4.462.3.1 View file →
@@ -1,5 +1,8 @@
1 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 * Post Types Admin
4 7 *
5 8 * @author PropertyHive
@@ -14,8 +17,9 @@
14 17
15 18 /**
16 19 * PH_Admin_Post_Types Class
17 20 */
21 +// phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedClassFound -- Legacy public global class PH_Admin_Post_Types; preserving the existing PH_* class name is required for plugin and extension compatibility.
18 22 class PH_Admin_Post_Types {
19 23
20 24 /**
21 25 * Constructor
@@ -22,8 +26,9 @@
22 26 */
23 27 public function __construct() {
24 28 add_action( 'admin_init', array( $this, 'include_post_type_handlers' ) );
25 29 add_filter( 'post_updated_messages', array( $this, 'post_updated_messages' ) );
30 + add_action( 'pre_get_posts', array( $this, 'refresh_property_office_filtering' ));
26 31 add_action( 'admin_print_scripts', array( $this, 'remove_month_filter' ) );
27 32 add_action( 'admin_print_scripts', array( $this, 'disable_autosave' ) );
28 33
29 34 // Filters
@@ -28,17 +33,277 @@
28 33
29 34 // Filters
30 35 add_action( 'restrict_manage_posts', array( $this, 'restrict_manage_posts' ) );
31 36 add_filter( 'request', array( $this, 'request_query' ) );
37 + add_filter( 'posts_join', array( $this, 'posts_join' ), 10, 2 );
38 + add_filter( 'posts_where', array( $this, 'posts_where' ), 10, 2 );
32 39
33 40 // Status transitions
34 41 add_action( 'delete_post', array( $this, 'delete_post' ) );
35 42 add_action( 'wp_trash_post', array( $this, 'trash_post' ) );
36 43 add_action( 'untrash_post', array( $this, 'untrash_post' ) );
44 +
45 + add_action( 'admin_init', array( $this, 'handle_archive_action' ) );
46 + add_action( 'admin_init', array( $this, 'handle_unarchive_action' ) );
47 +
48 + $post_types = array('property', 'contact', 'appraisal', 'viewing', 'offer', 'sale', 'tenancy', 'key_date');
49 + $post_types = apply_filters( 'propertyhive_post_types_with_archive', $post_types );
50 +
51 + foreach ( $post_types as $post_type )
52 + {
53 + add_filter( 'views_edit-' . $post_type, array( $this, 'adjust_post_status_views' ) );
54 + add_filter( "bulk_actions-edit-$post_type", array( $this, 'register_bulk_action_move_to_archive' ) );
55 + add_filter( "handle_bulk_actions-edit-$post_type", array( $this, 'handle_bulk_action_archive_and_unarchive' ), 10, 3 );
56 + }
57 +
58 + add_filter( 'post_row_actions', array( $this, 'modify_post_row_actions_for_archived' ), 10, 2 );
59 + }
60 +
61 + /**
62 + * Read one scalar admin query value after WordPress unslashes and sanitizes it.
63 + *
64 + * Admin list filters are read-only, but their values still flow into markup and
65 + * query arguments. Returning an empty value for arrays keeps scalar filters
66 + * from accidentally accepting a malformed request while preserving the
67 + * existing empty-filter behaviour.
68 + *
69 + * @param string $key Query-string key.
70 + * @return string
71 + */
72 + private function get_admin_query_value( $key ) {
73 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only admin list display or query; no state change.
74 + if ( ! isset( $_GET[ $key ] ) || ! is_scalar( $_GET[ $key ] ) ) {
75 + return '';
76 + }
77 +
78 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Read-only admin list value is copied, unslashed immediately below, and sanitized before use; the sniffer reports the source assignment instead of the sanitization boundary.
79 + $raw_value = $_GET[ $key ];
80 + $raw_value = wp_unslash( (string) $raw_value );
81 +
82 + return sanitize_text_field( $raw_value );
83 + }
84 +
85 + public function handle_bulk_action_archive_and_unarchive($redirect_to, $doaction, $post_ids)
86 + {
87 + if ($doaction === 'move_to_archive')
88 + {
89 + foreach ($post_ids as $post_id)
90 + {
91 + // Check permissions
92 + if (!current_user_can('edit_post', $post_id)) {
93 + continue;
94 + }
95 +
96 + // Update the post status to 'archive'
97 + $updated_post = array(
98 + 'ID' => $post_id,
99 + 'post_status' => 'archive',
100 + );
101 +
102 + wp_update_post($updated_post);
103 + }
104 +
105 + $redirect_to = add_query_arg('bulk_archived_posts', count($post_ids), $redirect_to);
106 + }
107 + elseif ($doaction === 'unarchive')
108 + {
109 + foreach ($post_ids as $post_id)
110 + {
111 + // Check permissions
112 + if (!current_user_can('edit_post', $post_id)) {
113 + continue;
114 + }
115 +
116 + // Update the post status to 'publish' (or whatever the original status should be)
117 + $updated_post = array(
118 + 'ID' => $post_id,
119 + 'post_status' => 'publish',
120 + );
121 +
122 + wp_update_post($updated_post);
123 + }
124 +
125 + $redirect_to = add_query_arg('bulk_unarchived_posts', count($post_ids), $redirect_to);
126 + }
127 +
128 + return $redirect_to;
129 + }
130 +
131 + public function register_bulk_action_move_to_archive( $bulk_actions )
132 + {
133 + global $post_status;
134 +
135 + // Define our custom actions
136 + $custom_actions = array();
137 +
138 + if ($post_status === 'archive') {
139 + $custom_actions['unarchive'] = __('Unarchive', 'propertyhive');
140 + } else {
141 + $custom_actions['move_to_archive'] = __('Move to Archive', 'propertyhive');
142 + }
143 +
144 + // Check if 'trash' exists and insert custom actions before it
145 + if (isset($bulk_actions['trash']))
146 + {
147 + $new_actions = array();
148 + foreach ($bulk_actions as $key => $value) {
149 + if ($key === 'trash') {
150 + $new_actions = array_merge($new_actions, $custom_actions);
151 + }
152 + $new_actions[$key] = $value;
153 + }
154 + return $new_actions;
155 + }
156 + elseif (isset($bulk_actions['untrash']))
157 + {
158 + $new_actions = array();
159 + foreach ($bulk_actions as $key => $value) {
160 + if ($key === 'untrash') {
161 + $new_actions = array_merge($new_actions, $custom_actions);
162 + }
163 + $new_actions[$key] = $value;
164 + }
165 + return $new_actions;
166 + }
167 + else
168 + {
169 + // If 'trash' doesn't exist, append custom actions at the end
170 + return array_merge($bulk_actions, $custom_actions);
171 + }
172 + }
173 +
174 + public function modify_post_row_actions_for_archived( $actions, $post )
175 + {
176 + // Define the post types that can be archived
177 + $post_types = array('property', 'contact', 'appraisal', 'viewing', 'offer', 'sale', 'tenancy', 'key_date');
178 + $post_types = apply_filters('propertyhive_post_types_with_archive', $post_types);
179 +
180 + // Check if the current post type is in the allowed post types and if the post is archived
181 + if ( in_array($post->post_type, $post_types) && $post->post_status == 'archive' )
182 + {
183 + // Remove the "View" link
184 + if (isset($actions['view'])) {
185 + unset($actions['view']);
186 + }
187 +
188 + // Add the "Unarchive" link
189 + $unarchive_url = wp_nonce_url(admin_url('post.php?post=' . $post->ID . '&action=unarchive&return=archive'), 'unarchive-post_' . $post->ID);
190 + $actions['unarchive'] = '<a href="' . esc_url($unarchive_url) . '">' . __('Unarchive', 'propertyhive') . '</a>';
191 + }
192 +
193 + return $actions;
194 + }
195 +
196 + public function adjust_post_status_views( $views )
197 + {
198 + if (isset($views['archive']))
199 + {
200 + $archive = $views['archive'];
201 + unset($views['archive']);
202 +
203 + $new_views = array();
204 + $bin_exists = false;
205 +
206 + foreach ($views as $key => $view) {
207 + if ($key === 'trash') {
208 + $bin_exists = true;
209 + $new_views['archive'] = $archive;
210 + }
211 + $new_views[$key] = $view;
212 + }
213 +
214 + // Ensure 'archive' is added to the end if 'trash' is not present
215 + if (!$bin_exists) {
216 + $new_views['archive'] = $archive;
217 + }
218 +
219 + return $new_views;
220 + }
221 +
222 + return $views;
223 + }
224 +
225 + public function handle_archive_action()
226 + {
227 + // Check if the action and nonce are set and valid
228 + if ( !isset($_GET['action']) || $_GET['action'] !== 'archive_single' )
229 + return;
37 230
231 + $post_id = isset($_GET['post']) ? intval($_GET['post']) : 0;
232 + $post_type = get_post_type($post_id);
233 +
234 + if ( !wp_verify_nonce( ( isset( $_GET['_wpnonce'] ) && is_string( $_GET['_wpnonce'] ) ) ? sanitize_text_field( wp_unslash( $_GET['_wpnonce'] ) ) : '', 'archive-post_' . $post_id) )
235 + {
236 + wp_die(esc_html(__('Security check failed.', 'propertyhive')));
237 + }
238 +
239 + if ( !current_user_can('edit_post', $post_id) )
240 + {
241 + wp_die(esc_html(__('You do not have permission to edit this post.', 'propertyhive')));
242 + }
243 +
244 + // Update the post status to 'archive'
245 + $updated_post = array(
246 + 'ID' => $post_id,
247 + 'post_status' => 'archive',
248 + );
249 +
250 + $result = wp_update_post($updated_post, true);
251 +
252 + if ( is_wp_error($result) )
253 + {
254 + wp_die(esc_html(__('An error occurred while archiving the post.', 'propertyhive')));
255 + }
256 +
257 + // Redirect to the main list of contacts
258 + wp_safe_redirect(admin_url('edit.php?post_type=' . $post_type));
259 + exit;
260 + }
261 +
262 + public function handle_unarchive_action()
263 + {
264 + // Check if the action and nonce are set and valid
265 + if ( !isset($_GET['action']) || $_GET['action'] !== 'unarchive_single' )
266 + return;
38 267
39 - }
268 + $post_id = isset($_GET['post']) ? intval($_GET['post']) : 0;
269 + $post_type = get_post_type($post_id);
40 270
271 + if ( !wp_verify_nonce( ( isset( $_GET['_wpnonce'] ) && is_string( $_GET['_wpnonce'] ) ) ? sanitize_text_field( wp_unslash( $_GET['_wpnonce'] ) ) : '', 'unarchive-post_' . $post_id) )
272 + {
273 + wp_die(esc_html(__('Security check failed.', 'propertyhive')));
274 + }
275 +
276 + if ( !current_user_can('edit_post', $post_id) )
277 + {
278 + wp_die(esc_html(__('You do not have permission to edit this post.', 'propertyhive')));
279 + }
280 +
281 + // Update the post status to 'publish'
282 + $updated_post = array(
283 + 'ID' => $post_id,
284 + 'post_status' => 'publish',
285 + );
286 +
287 + $result = wp_update_post($updated_post, true);
288 +
289 + if ( is_wp_error($result) )
290 + {
291 + wp_die(esc_html(__('An error occurred while unarchiving the post.', 'propertyhive')));
292 + }
293 +
294 + // Redirect to the main list of contacts
295 + if ( isset($_GET['return']) && $_GET['return'] === 'archive' )
296 + {
297 + wp_safe_redirect(admin_url('edit.php?post_status=archive&post_type=' . get_post_type($post_id)));
298 + }
299 + else
300 + {
301 + wp_safe_redirect(admin_url('edit.php?post_type=' . get_post_type($post_id)));
302 + }
303 + exit;
304 + }
305 +
41 306 /**
42 307 * Conditonally load classes and functions only needed when viewing a post type.
43 308 */
44 309 public function include_post_type_handlers() {
@@ -52,8 +317,10 @@
52 317 include( 'post-types/class-ph-admin-cpt-appraisal.php' );
53 318 include( 'post-types/class-ph-admin-cpt-viewing.php' );
54 319 include( 'post-types/class-ph-admin-cpt-offer.php' );
55 320 include( 'post-types/class-ph-admin-cpt-sale.php' );
321 + include( 'post-types/class-ph-admin-cpt-tenancy.php' );
322 + include( 'post-types/class-ph-admin-cpt-key-date.php' );
56 323 }
57 324
58 325 /**
59 326 * Change messages when a post type is updated.
@@ -65,19 +332,24 @@
65 332 global $post, $post_ID;
66 333
67 334 $messages['property'] = array(
68 335 0 => '', // Unused. Messages start at index 1.
69 - 1 => sprintf( __( 'Property updated. <a href="%s">View Property</a>', 'propertyhive' ), esc_url( get_permalink($post_ID) ) ),
336 + /* translators: %s: URL to view the property */
337 + 1 => sprintf( __( 'Property updated. <a href="%s">View property</a>', 'propertyhive' ), esc_url( get_permalink($post_ID) ) ),
70 338 2 => __( 'Custom field updated.', 'propertyhive' ),
71 339 3 => __( 'Custom field deleted.', 'propertyhive' ),
72 340 4 => __( 'Property updated.', 'propertyhive' ),
73 - 5 => isset($_GET['revision']) ? sprintf( __( 'Property restored to revision from %s', 'propertyhive' ), wp_post_revision_title( (int) $_GET['revision'], false ) ) : false,
74 - 6 => sprintf( __( 'Property published. <a href="%s">View Property</a>', 'propertyhive' ), esc_url( get_permalink($post_ID) ) ),
341 + 5 => __( 'Revision restored.', 'propertyhive' ),
342 + /* translators: %s: URL to view the property */
343 + 6 => sprintf( __( 'Property published. <a href="%s">View property</a>', 'propertyhive' ), esc_url( get_permalink($post_ID) ) ),
75 344 7 => __( 'Property saved.', 'propertyhive' ),
76 - 8 => sprintf( __( 'Property submitted. <a target="_blank" href="%s">Preview Property</a>', 'propertyhive' ), esc_url( add_query_arg( 'preview', 'true', get_permalink($post_ID) ) ) ),
77 - 9 => sprintf( __( 'Property scheduled for: <strong>%1$s</strong>. <a target="_blank" href="%2$s">Preview Property</a>', 'propertyhive' ),
345 + /* translators: %s: URL to preview the property */
346 + 8 => sprintf( __( 'Property submitted. <a target="_blank" href="%s">Preview property</a>', 'propertyhive' ), esc_url( add_query_arg( 'preview', 'true', get_permalink($post_ID) ) ) ),
347 + /* translators: 1: formatted date, 2: URL to preview the property */
348 + 9 => sprintf( __( 'Property scheduled for: <strong>%1$s</strong>. <a target="_blank" href="%2$s">Preview property</a>', 'propertyhive' ),
78 349 date_i18n( __( 'M j, Y @ G:i', 'propertyhive' ), strtotime( $post->post_date ) ), esc_url( get_permalink($post_ID) ) ),
79 - 10 => sprintf( __( 'Property draft updated. <a target="_blank" href="%s">Preview Property</a>', 'propertyhive' ), esc_url( add_query_arg( 'preview', 'true', get_permalink($post_ID) ) ) ),
350 + /* translators: %s: URL to preview the property */
351 + 10 => sprintf( __( 'Property draft updated. <a target="_blank" href="%s">Preview property</a>', 'propertyhive' ), esc_url( add_query_arg( 'preview', 'true', get_permalink($post_ID) ) ) ),
80 352 );
81 353
82 354 $messages['contact'] = array(
83 355 0 => '', // Unused. Messages start at index 1.
@@ -84,12 +356,13 @@
84 356 1 => __( 'Contact updated.', 'propertyhive' ),
85 357 2 => __( 'Custom field updated.', 'propertyhive' ),
86 358 3 => __( 'Custom field deleted.', 'propertyhive' ),
87 359 4 => __( 'Contact updated.', 'propertyhive' ),
88 - 5 => isset($_GET['revision']) ? sprintf( __( 'Contact restored to revision from %s', 'propertyhive' ), wp_post_revision_title( (int) $_GET['revision'], false ) ) : false,
360 + 5 => __( 'Revision restored.', 'propertyhive' ),
89 361 6 => __( 'Contact published.', 'propertyhive' ),
90 362 7 => __( 'Contact saved.', 'propertyhive' ),
91 363 8 => __( 'Contact submitted.', 'propertyhive' ),
364 + /* translators: 1: formatted date */
92 365 9 => sprintf( __( 'Contact scheduled for: <strong>%1$s</strong>.', 'propertyhive' ), date_i18n( __( 'M j, Y @ G:i', 'propertyhive' ), strtotime( $post->post_date ) )),
93 366 10 => __( 'Contact draft updated.', 'propertyhive' ),
94 367 );
95 368
@@ -98,12 +371,13 @@
98 371 1 => __( 'Office updated.', 'propertyhive' ),
99 372 2 => __( 'Custom field updated.', 'propertyhive' ),
100 373 3 => __( 'Custom field deleted.', 'propertyhive' ),
101 374 4 => __( 'Office updated.', 'propertyhive' ),
102 - 5 => isset($_GET['revision']) ? sprintf( __( 'Office restored to revision from %s', 'propertyhive' ), wp_post_revision_title( (int) $_GET['revision'], false ) ) : false,
375 + 5 => __( 'Revision restored.', 'propertyhive' ),
103 376 6 => sprintf( __( 'Office published.', 'propertyhive' ), esc_url( get_permalink($post_ID) ) ),
104 377 7 => __( 'Office saved.', 'propertyhive' ),
105 378 8 => sprintf( __( 'Office submitted.', 'propertyhive' ), esc_url( add_query_arg( 'preview', 'true', get_permalink($post_ID) ) ) ),
379 + /* translators: 1: formatted date */
106 380 9 => sprintf( __( 'Office scheduled for: <strong>%1$s</strong>.', 'propertyhive' ),
107 381 date_i18n( __( 'M j, Y @ G:i', 'propertyhive' ), strtotime( $post->post_date ) ), esc_url( get_permalink($post_ID) ) ),
108 382 10 => sprintf( __( 'Office draft updated. ', 'propertyhive' ), esc_url( add_query_arg( 'preview', 'true', get_permalink($post_ID) ) ) ),
109 383 );
@@ -113,12 +387,13 @@
113 387 1 => sprintf( __( 'Enquiry updated.', 'propertyhive' ), esc_url( get_permalink($post_ID) ) ),
114 388 2 => __( 'Custom field updated.', 'propertyhive' ),
115 389 3 => __( 'Custom field deleted.', 'propertyhive' ),
116 390 4 => __( 'Enquiry updated.', 'propertyhive' ),
117 - 5 => isset($_GET['revision']) ? sprintf( __( 'Enquiry restored to revision from %s', 'propertyhive' ), wp_post_revision_title( (int) $_GET['revision'], false ) ) : false,
391 + 5 => __( 'Revision restored.', 'propertyhive' ),
118 392 6 => sprintf( __( 'Enquiry published.', 'propertyhive' ), esc_url( get_permalink($post_ID) ) ),
119 393 7 => __( 'Enquiry saved.', 'propertyhive' ),
120 394 8 => sprintf( __( 'Enquiry submitted.', 'propertyhive' ), esc_url( add_query_arg( 'preview', 'true', get_permalink($post_ID) ) ) ),
395 + /* translators: 1: formatted date */
121 396 9 => sprintf( __( 'Enquiry scheduled for: <strong>%1$s</strong>.', 'propertyhive' ),
122 397 date_i18n( __( 'M j, Y @ G:i', 'propertyhive' ), strtotime( $post->post_date ) ), esc_url( get_permalink($post_ID) ) ),
123 398 10 => sprintf( __( 'Enquiry draft updated.', 'propertyhive' ), esc_url( add_query_arg( 'preview', 'true', get_permalink($post_ID) ) ) ),
124 399 );
@@ -130,10 +405,13 @@
130 405 * Remove month filter from some property hive pages
131 406 */
132 407 public function remove_month_filter() {
133 408 global $typenow;
134 -
135 - if ($typenow == 'property' || $typenow == 'contact' || $typenow == 'appraisal' || $typenow == 'viewing' || $typenow == 'offer' || $typenow == 'sale')
409 +
410 + $post_types_to_hide_months_dropdown = array('property', 'contact', 'enquiry', 'appraisal', 'viewing', 'offer', 'sale', 'tenancy', 'key_date');
411 + $post_types_to_hide_months_dropdown = apply_filters( 'propertyhive_post_types_to_hide_months_dropdown', $post_types_to_hide_months_dropdown );
412 +
413 + if ( in_array($typenow, $post_types_to_hide_months_dropdown) )
136 414 {
137 415 add_filter('months_dropdown_results', '__return_empty_array');
138 416 }
139 417 }
@@ -173,8 +451,20 @@
173 451 break;
174 452 case 'viewing' :
175 453 $this->viewing_filters();
176 454 break;
455 + case 'offer' :
456 + $this->offer_filters();
457 + break;
458 + case 'sale' :
459 + $this->sale_filters();
460 + break;
461 + case 'tenancy' :
462 + $this->tenancy_filters();
463 + break;
464 + case 'key_date' :
465 + $this->key_date_filters();
466 + break;
177 467 default :
178 468 break;
179 469 }
180 470 }
@@ -192,10 +482,11 @@
192 482 $output .= $this->property_marketing_filter();
193 483 $output .= $this->property_availability_filter();
194 484 $output .= $this->property_location_filter();
195 485 $output .= $this->property_office_filter();
196 - $output .= $this->property_negotiator_filter();
486 + $output .= $this->negotiator_filter();
197 487
488 + // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Built-in controls escape their text and attributes before this trusted PHP filter adds complete HTML controls.
198 489 echo apply_filters( 'propertyhive_property_filters', $output );
199 490 }
200 491
201 492 /**
@@ -205,22 +496,24 @@
205 496 global $wp_query;
206 497
207 498 $departments = ph_get_departments();
208 499
209 - $selected_department = isset( $_GET['_department'] ) && in_array( $_GET['_department'], array_keys($departments) ) ? $_GET['_department'] : '';
500 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only admin list display or query; no state change.
501 + $requested_value = isset( $_GET['_department'] ) && is_string( $_GET['_department'] ) ? sanitize_text_field( wp_unslash( $_GET['_department'] ) ) : '';
502 + $selected_department = array_key_exists( $requested_value, $departments ) ? $requested_value : '';
210 503
211 504 // Department filtering
212 505 $output = '<select name="_department" id="dropdown_property_department">';
213 506
214 - $output .= '<option value="">' . __( 'All Departments', 'propertyhive' ) . '</option>';
507 + $output .= '<option value="">' . esc_html__( 'All Departments', 'propertyhive' ) . '</option>';
215 508
216 509 foreach ( $departments as $key => $value )
217 510 {
218 511 if ( get_option( 'propertyhive_active_departments_' . str_replace("residential-", "", $key) ) == 'yes' )
219 512 {
220 - $output .= '<option value="' . $key . '"';
513 + $output .= '<option value="' . esc_attr($key) . '"';
221 514 $output .= selected( $key, $selected_department, false );
222 - $output .= '>' . $value . '</option>';
515 + $output .= '>' . esc_html($value) . '</option>';
223 516 }
224 517 }
225 518
226 519 $output .= '</select>';
@@ -236,9 +529,9 @@
236 529
237 530 // Department filtering
238 531 $output = '<select name="_office_id" id="dropdown_property_office_id">';
239 532
240 - $output .= '<option value="">' . __( 'All Offices', 'propertyhive' ) . '</option>';
533 + $output .= '<option value="">' . esc_html__( 'All Offices', 'propertyhive' ) . '</option>';
241 534
242 535 $args = array(
243 536 'post_type' => 'office',
244 537 'nopaging' => true,
@@ -252,14 +545,16 @@
252 545 while ($office_query->have_posts())
253 546 {
254 547 $office_query->the_post();
255 548
256 - $output .= '<option value="' . $post->ID . '"';
549 + $output .= '<option value="' . esc_attr($post->ID) . '"';
550 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only admin list display or query; no state change.
257 551 if ( isset( $_GET['_office_id'] ) && ! empty( $_GET['_office_id'] ) )
258 552 {
553 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only admin list display or query; no state change.
259 554 $output .= selected( $post->ID, (int)$_GET['_office_id'], false );
260 555 }
261 - $output .= '>' . get_the_title() . '</option>';
556 + $output .= '>' . esc_html(get_the_title()) . '</option>';
262 557 }
263 558 }
264 559
265 560 wp_reset_postdata();
@@ -269,32 +564,51 @@
269 564 return $output;
270 565 }
271 566
272 567 /**
273 - * Show a property negotiator filter box
568 + * Show a negotiator filter box
274 569 */
275 - public function property_negotiator_filter() {
276 - global $wp_query, $post;
277 -
278 - $selected = '';
279 - if ( isset( $_GET['_negotiator_id'] ) && ! empty( $_GET['_negotiator_id'] ) )
280 - {
281 - $selected = (int)$_GET['_negotiator_id'];
282 - }
283 -
284 - $args = array(
570 + public function negotiator_filter() {
571 +
572 + return wp_dropdown_users(array(
285 573 'name' => '_negotiator_id',
286 574 'id' => 'dropdown_property_negotiator_id',
287 - 'show_option_all' => __( 'All Negotiators', 'propertyhive' ),
288 - 'selected' => $selected,
575 + 'show_option_all' => esc_html__( 'All Negotiators', 'propertyhive' ),
576 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only admin list display or query; no state change.
577 + 'selected' => empty( $_GET['_negotiator_id'] ) ? '' : (int)$_GET['_negotiator_id'],
289 578 'echo' => false,
290 - 'role__not_in' => array('property_hive_contact')
291 - );
292 - $output = wp_dropdown_users($args);
579 + // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Legacy Property Negotiator compatibility filter; existing role filters depend on this exact public hook name.
580 + 'role__not_in' => apply_filters( 'property_negotiator_exclude_roles', array('property_hive_contact', 'subscriber') )
581 + ));
582 + }
293 583
294 - return $output;
295 - }
584 + /**
585 + * Show a date range selector
586 + */
587 + public function date_range_filter() {
296 588
589 + $date_range_label = $this->get_admin_query_value( '_date_range_label' );
590 + $date_range_label = empty( $date_range_label ) ? __( 'Any Time', 'propertyhive' ) : $date_range_label;
591 +
592 + // The date picker doesn't have a concept of 'Any Time', so valid dates must be used
593 + // I've used the last and first date of the month (reversed) as it's a range that is not selectable, but is within the current month
594 + // If I used an already labelled date range (e.g. 'Today'), it would show as 'Today' when selected
595 + // If I use a nearby date range (e.g. 'Yesterday'), if someone actually selected that range it would show as 'Any Time'
596 + // If I use a unlikely date range (e.g. 01-01-1970 - 31-12-2070), the custom date range picker would open showing Jan 1970.
597 + $date_range_from = $this->get_admin_query_value( '_date_range_from' );
598 + $date_range_from = empty( $date_range_from ) ? gmdate('Y-m-d', strtotime('last day of this month')) : $date_range_from;
599 + $date_range_to = $this->get_admin_query_value( '_date_range_to' );
600 + $date_range_to = empty( $date_range_to ) ? gmdate('Y-m-d', strtotime('first day of this month')) : $date_range_to;
601 +
602 + return "
603 + <select name='_date_range_label' id='date_range' style='max-width:25rem;'>
604 + <option selected>" . esc_html($date_range_label) . "</option>
605 + <select/>
606 + <input type='hidden' name='_date_range_from' id='date_range_from' value='" . esc_attr($date_range_from) . "'>
607 + <input type='hidden' name='_date_range_to' id='date_range_to' value='" . esc_attr($date_range_to) . "'>
608 + ";
609 + }
610 +
297 611 /**
298 612 * Show a property location filter box
299 613 */
300 614 public function property_location_filter() {
@@ -307,9 +621,9 @@
307 621 $args = array(
308 622 'hide_empty' => false,
309 623 'parent' => 0
310 624 );
311 - $terms = get_terms( 'location', $args );
625 + $terms = get_terms( array_merge( wp_parse_args( $args ), array( 'taxonomy' => 'location' ) ) );
312 626
313 627 if ( !empty( $terms ) && !is_wp_error( $terms ) )
314 628 {
315 629 foreach ($terms as $term)
@@ -319,9 +633,9 @@
319 633 $args = array(
320 634 'hide_empty' => false,
321 635 'parent' => $term->term_id
322 636 );
323 - $subterms = get_terms( 'location', $args );
637 + $subterms = get_terms( array_merge( wp_parse_args( $args ), array( 'taxonomy' => 'location' ) ) );
324 638
325 639 if ( !empty( $subterms ) && !is_wp_error( $subterms ) )
326 640 {
327 641 foreach ($subterms as $term)
@@ -331,9 +645,9 @@
331 645 $args = array(
332 646 'hide_empty' => false,
333 647 'parent' => $term->term_id
334 648 );
335 - $subsubterms = get_terms( 'location', $args );
649 + $subsubterms = get_terms( array_merge( wp_parse_args( $args ), array( 'taxonomy' => 'location' ) ) );
336 650
337 651 if ( !empty( $subsubterms ) && !is_wp_error( $subsubterms ) )
338 652 {
339 653 foreach ($subsubterms as $term)
@@ -345,20 +659,22 @@
345 659 }
346 660 }
347 661 }
348 662
349 - $output .= '<option value="">' . __( 'All Locations', 'propertyhive' ) . '</option>';
663 + $output .= '<option value="">' . esc_html(__( 'All Locations', 'propertyhive' )) . '</option>';
350 664
351 665 if ( !empty($options) )
352 666 {
353 667 foreach ( $options as $value => $label )
354 668 {
355 - $output .= '<option value="' . $value . '"';
669 + $output .= '<option value="' . esc_attr($value) . '"';
670 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only admin list display or query; no state change.
356 671 if ( isset( $_GET['_location_id'] ) && ! empty( $_GET['_location_id'] ) )
357 672 {
673 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only admin list display or query; no state change.
358 674 $output .= selected( $value, (int)$_GET['_location_id'], false );
359 675 }
360 - $output .= '>' . $label . '</option>';
676 + $output .= '>' . esc_html($label) . '</option>';
361 677 }
362 678 }
363 679
364 680 $output .= '</select>';
@@ -379,9 +695,9 @@
379 695 $args = array(
380 696 'hide_empty' => false,
381 697 'parent' => 0
382 698 );
383 - $terms = get_terms( 'availability', $args );
699 + $terms = get_terms( array_merge( wp_parse_args( $args ), array( 'taxonomy' => 'availability' ) ) );
384 700
385 701 if ( !empty( $terms ) && !is_wp_error( $terms ) )
386 702 {
387 703 foreach ($terms as $term)
@@ -389,20 +705,22 @@
389 705 $options[$term->term_id] = $term->name;
390 706 }
391 707 }
392 708
393 - $output .= '<option value="">' . __( 'All Availabilities', 'propertyhive' ) . '</option>';
709 + $output .= '<option value="">' . esc_html(__( 'All Availabilities', 'propertyhive' )) . '</option>';
394 710
395 711 if ( !empty($options) )
396 712 {
397 713 foreach ( $options as $value => $label )
398 714 {
399 - $output .= '<option value="' . $value . '"';
715 + $output .= '<option value="' . esc_attr($value) . '"';
716 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only admin list display or query; no state change.
400 717 if ( isset( $_GET['_availability_id'] ) && ! empty( $_GET['_availability_id'] ) )
401 718 {
719 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only admin list display or query; no state change.
402 720 $output .= selected( $value, (int)$_GET['_availability_id'], false );
403 721 }
404 - $output .= '>' . $label . '</option>';
722 + $output .= '>' . esc_html($label) . '</option>';
405 723 }
406 724 }
407 725
408 726 $output .= '</select>';
@@ -418,9 +736,9 @@
418 736
419 737 // Availability filtering
420 738 $output = '<select name="_marketing" id="dropdown_property_marketing">';
421 739
422 - $output .= '<option value="">' . __( 'All Marketing Statuses', 'propertyhive' ) . '</option>';
740 + $output .= '<option value="">' . esc_html__( 'All Marketing Statuses', 'propertyhive' ) . '</option>';
423 741
424 742 $options = array(
425 743 'on_market' => __( 'On Market Only', 'propertyhive' ),
426 744 'off_market' => __( 'Not On Market Only', 'propertyhive' ),
@@ -430,9 +748,9 @@
430 748 $args = array(
431 749 'hide_empty' => false,
432 750 'parent' => 0
433 751 );
434 - $terms = get_terms( 'marketing_flag', $args );
752 + $terms = get_terms( array_merge( wp_parse_args( $args ), array( 'taxonomy' => 'marketing_flag' ) ) );
435 753
436 754 if ( !empty( $terms ) && !is_wp_error( $terms ) )
437 755 {
438 756 foreach ($terms as $term)
@@ -441,17 +759,18 @@
441 759 }
442 760 }
443 761
444 762 $options = apply_filters( 'propertyhive_property_filter_marketing_options', $options );
763 + $selected_marketing = $this->get_admin_query_value( '_marketing' );
445 764
446 765 foreach ( $options as $key => $value )
447 766 {
448 - $output .= '<option value="' . $key . '"';
449 - if ( isset( $_GET['_marketing'] ) && ! empty( $_GET['_marketing'] ) )
767 + $output .= '<option value="' . esc_attr($key) . '"';
768 + if ( ! empty( $selected_marketing ) )
450 769 {
451 - $output .= selected( $key, sanitize_text_field($_GET['_marketing']), false );
770 + $output .= selected( $key, $selected_marketing, false );
452 771 }
453 - $output .= '>' . $value . '</option>';
772 + $output .= '>' . esc_html($value) . '</option>';
454 773 }
455 774
456 775 $output .= '</select>';
457 776
@@ -463,9 +782,11 @@
463 782 */
464 783 public function contact_filters() {
465 784 global $wp_query;
466 785
467 - $selected_contact_type = isset( $_GET['_contact_type'] ) && in_array( $_GET['_contact_type'], array( 'owner', 'potentialowner', 'applicant', 'thirdparty' ) ) ? $_GET['_contact_type'] : '';
786 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only admin list display or query; no state change.
787 + $requested_value = isset( $_GET['_contact_type'] ) && is_string( $_GET['_contact_type'] ) ? sanitize_text_field( wp_unslash( $_GET['_contact_type'] ) ) : '';
788 + $selected_contact_type = in_array( $requested_value, array( 'owner', 'potentialowner', 'applicant', 'hotapplicant', 'thirdparty' ), true ) ? $requested_value : '';
468 789
469 790 // Type filtering
470 791 $options = array();
471 792
@@ -471,9 +792,9 @@
471 792
472 793 // Owners
473 794 $option = '<option value="owner"';
474 795 $option .= selected( 'owner', $selected_contact_type, false );
475 - $option .= '>' . __( 'Owners and Landlords', 'propertyhive' ) . '</option>';
796 + $option .= '>' . esc_html(__( 'Owners and Landlords', 'propertyhive' )) . '</option>';
476 797
477 798 $options[] = $option;
478 799
479 800 // Potential Owners
@@ -478,9 +799,9 @@
478 799
479 800 // Potential Owners
480 801 $option = '<option value="potentialowner"';
481 802 $option .= selected( 'potentialowner', $selected_contact_type, false );
482 - $option .= '>' . __( 'Potential Owners and Landlords', 'propertyhive' ) . '</option>';
803 + $option .= '>' . esc_html(__( 'Potential Owners and Landlords', 'propertyhive' )) . '</option>';
483 804
484 805 $options[] = $option;
485 806
486 807 // Applicants
@@ -485,16 +806,23 @@
485 806
486 807 // Applicants
487 808 $option = '<option value="applicant"';
488 809 $option .= selected( 'applicant', $selected_contact_type, false );
489 - $option .= '>' . __( 'Applicants', 'propertyhive' ) . '</option>';
810 + $option .= '>' . esc_html(__( 'Applicants', 'propertyhive' )) . '</option>';
490 811
491 812 $options[] = $option;
492 813
814 + // Hot Applicants
815 + $option = '<option value="hotapplicant"';
816 + $option .= selected( 'hotapplicant', $selected_contact_type, false );
817 + $option .= '>- ' . esc_html(__( 'Hot Applicants', 'propertyhive' )) . '</option>';
818 +
819 + $options[] = $option;
820 +
493 821 // Third Parties
494 822 $option = '<option value="thirdparty"';
495 823 $option .= selected( 'thirdparty', $selected_contact_type, false );
496 - $option .= '>' . __( 'Third Party Contacts', 'propertyhive' ) . '</option>';
824 + $option .= '>' . esc_html(__( 'Third Party Contacts', 'propertyhive' )) . '</option>';
497 825
498 826 $options[] = $option;
499 827
500 828 $options = apply_filters( 'propertyhive_contact_filter_options', $options );
@@ -503,9 +831,9 @@
503 831 if (count($options) > 1)
504 832 {
505 833 $output = '<select name="_contact_type" id="dropdown_contact_type">';
506 834
507 - $output .= '<option value="">' . __( 'Show all contact types', 'propertyhive' ) . '</option>';
835 + $output .= '<option value="">' . esc_html(__( 'Show all contact types', 'propertyhive' )) . '</option>';
508 836
509 837 $output .= implode("", $options);
510 838
511 839 $output .= '</select>';
@@ -510,9 +838,12 @@
510 838
511 839 $output .= '</select>';
512 840 }
513 841
514 - echo $output;
842 + $output .= $this->date_range_filter('Date Created');
843 +
844 + // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Built-in controls escape their text and attributes before this trusted PHP filter adds complete HTML controls.
845 + echo apply_filters( 'propertyhive_contact_filters', $output );
515 846 }
516 847
517 848 /**
518 849 * Show an enquiry filter box
@@ -522,11 +853,15 @@
522 853
523 854 // Department filtering
524 855 $output = '';
525 856
857 + $output .= $this->date_range_filter();
526 858 $output .= $this->enquiry_status_filter();
527 859 $output .= $this->enquiry_source_filter();
860 + $output .= $this->enquiry_office_filter();
861 + $output .= $this->enquiry_negotiator_filter();
528 862
863 + // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Built-in controls escape their text and attributes before this trusted PHP filter adds complete HTML controls.
529 864 echo apply_filters( 'propertyhive_enquiry_filters', $output );
530 865 }
531 866
532 867 /**
@@ -534,21 +869,30 @@
534 869 */
535 870 public function enquiry_status_filter() {
536 871 global $wp_query;
537 872
538 - $selected_status = isset( $_GET['_status'] ) && in_array( $_GET['_status'], array( 'open', 'closed' ) ) ? $_GET['_status'] : '';
539 -
873 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only admin list display or query; no state change.
874 + $requested_value = isset( $_GET['_status'] ) && is_string( $_GET['_status'] ) ? sanitize_text_field( wp_unslash( $_GET['_status'] ) ) : '';
875 + $selected_status = in_array( $requested_value, array( 'all', 'open', 'closed' ), true ) ? $requested_value : '';
876 +
540 877 // Status filtering
541 - $output = '<select name="_status" id="dropdown_enquiry_status">';
542 -
543 - $output .= '<option value="open"';
544 - $output .= selected( 'open', $selected_status, false );
545 - $output .= '>' . __( 'Open', 'propertyhive' ) . '</option>';
878 + $output = '<select name="_status" id="dropdown_enquiry_status">
879 + <option value="all"' . selected( 'all', $selected_status, false ) . '>All</option>';
546 880
547 - $output .= '<option value="closed"';
548 - $output .= selected( 'closed', $selected_status, false );
549 - $output .= '>' . __( 'Closed', 'propertyhive' ) . '</option>';
550 -
881 + $enquiry_statuses = ph_get_enquiry_statuses();
882 +
883 + foreach ( $enquiry_statuses as $status => $display_status )
884 + {
885 + $output .= '<option value="' . esc_attr($status) . '"';
886 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only admin list display or query; no state change.
887 + if ( $status == $selected_status || ( $status == 'open' && ( !isset($_GET['_status']) || empty($_GET['_status']) ) ) )
888 + {
889 + $output .= ' selected';
890 + }
891 + $output .= selected( $status, $selected_status, false );
892 + $output .= '>' . esc_html($display_status) . '</option>';
893 + }
894 +
551 895 $output .= '</select>';
552 896
553 897 return $output;
554 898 }
@@ -564,22 +908,25 @@
564 908 'website' => __( 'Website', 'propertyhive' )
565 909 );
566 910
567 911 $sources = apply_filters( 'propertyhive_enquiry_sources', $sources );
912 +
913 + asort($sources);
568 914
569 915 // Status filtering
570 916 $output = '<select name="_source" id="dropdown_enquiry_source">';
917 + $selected_source = $this->get_admin_query_value( '_source' );
571 918
572 - $output .= '<option value="">' . __( 'Show all sources', 'propertyhive' ) . '</option>';
919 + $output .= '<option value="">' . esc_html__( 'Show all sources', 'propertyhive' ) . '</option>';
573 920
574 921 foreach ( $sources as $key => $value )
575 922 {
576 - $output .= '<option value="' . $key . '"';
577 - if ( isset( $_GET['_source'] ) && ! empty( $_GET['_source'] ) )
923 + $output .= '<option value="' . esc_attr($key) . '"';
924 + if ( ! empty( $selected_source ) )
578 925 {
579 - $output .= selected( $key, sanitize_text_field($_GET['_source']), false );
926 + $output .= selected( $key, $selected_source, false );
580 927 }
581 - $output .= '>' . __( $value, 'propertyhive' ) . '</option>';
928 + $output .= '>' . esc_html( $value ) . '</option>';
582 929 }
583 930
584 931 $output .= '</select>';
585 932
@@ -586,8 +933,67 @@
586 933 return $output;
587 934 }
588 935
589 936 /**
937 + * Show an enquiry office filter box
938 + */
939 + public function enquiry_office_filter() {
940 + global $wp_query, $post;
941 +
942 + // Department filtering
943 + $output = '<select name="_office_id" id="dropdown_enquiry_office_id">';
944 +
945 + $output .= '<option value="">' . esc_html__( 'All Offices', 'propertyhive' ) . '</option>';
946 +
947 + $args = array(
948 + 'post_type' => 'office',
949 + 'nopaging' => true,
950 + 'orderby' => 'title',
951 + 'order' => 'ASC'
952 + );
953 + $office_query = new WP_Query($args);
954 +
955 + if ($office_query->have_posts())
956 + {
957 + while ($office_query->have_posts())
958 + {
959 + $office_query->the_post();
960 +
961 + $output .= '<option value="' . esc_attr($post->ID) . '"';
962 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only admin list display or query; no state change.
963 + if ( isset( $_GET['_office_id'] ) && ! empty( $_GET['_office_id'] ) )
964 + {
965 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only admin list display or query; no state change.
966 + $output .= selected( $post->ID, (int)$_GET['_office_id'], false );
967 + }
968 + $output .= '>' . esc_html(get_the_title()) . '</option>';
969 + }
970 + }
971 +
972 + wp_reset_postdata();
973 +
974 + $output .= '</select>';
975 +
976 + return $output;
977 + }
978 +
979 + /**
980 + * Show an enquiry negotiator filter box
981 + */
982 + public function enquiry_negotiator_filter() {
983 + return wp_dropdown_users(array(
984 + 'name' => '_negotiator_id',
985 + 'id' => 'dropdown_enquiry_negotiator_id',
986 + 'show_option_all' => esc_html__( 'All Negotiators', 'propertyhive' ),
987 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only admin list display or query; no state change.
988 + 'selected' => empty( $_GET['_negotiator_id'] ) ? '' : (int)$_GET['_negotiator_id'],
989 + 'echo' => false,
990 + // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Legacy Property Negotiator compatibility filter; existing role filters depend on this exact public hook name.
991 + 'role__not_in' => apply_filters( 'property_negotiator_exclude_roles', array('property_hive_contact', 'subscriber') )
992 + ));
993 + }
994 +
995 + /**
590 996 * Show am appraisal filter box
591 997 */
592 998 public function appraisal_filters() {
593 999 global $wp_query;
@@ -594,10 +1000,12 @@
594 1000
595 1001 $output = '';
596 1002
597 1003 $output .= $this->appraisal_status_filter();
598 - $output .= $this->appraisal_attending_negotiator_filter();
1004 + $output .= $this->negotiator_filter();
1005 + $output .= $this->date_range_filter();
599 1006
1007 + // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Built-in controls escape their text and attributes before this trusted PHP filter adds complete HTML controls.
600 1008 echo apply_filters( 'propertyhive_appraisal_filters', $output );
601 1009 }
602 1010
603 1011 /**
@@ -605,38 +1013,40 @@
605 1013 */
606 1014 public function appraisal_status_filter() {
607 1015 global $wp_query;
608 1016
609 - $selected_status = isset( $_GET['_status'] ) && in_array( $_GET['_status'], array( 'pending', 'carried_out', 'won', 'lost', 'instructed', 'cancelled' ) ) ? $_GET['_status'] : '';
1017 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only admin list display or query; no state change.
1018 + $requested_value = isset( $_GET['_status'] ) && is_string( $_GET['_status'] ) ? sanitize_text_field( wp_unslash( $_GET['_status'] ) ) : '';
1019 + $selected_status = in_array( $requested_value, array( 'pending', 'carried_out', 'won', 'lost', 'instructed', 'cancelled' ), true ) ? $requested_value : '';
610 1020
611 1021 // Status filtering
612 1022 $output = '<select name="_status" id="dropdown_appraisal_status">';
613 1023
614 - $output .= '<option value="">All Statuses</option>';
1024 + $output .= '<option value="">' . esc_html__( 'All Statuses', 'propertyhive' ) . '</option>';
615 1025
616 1026 $output .= '<option value="pending"';
617 1027 $output .= selected( 'pending', $selected_status, false );
618 - $output .= '>' . __( 'Pending', 'propertyhive' ) . '</option>';
1028 + $output .= '>' . esc_html(__( 'Pending', 'propertyhive' )) . '</option>';
619 1029
620 1030 $output .= '<option value="carried_out"';
621 1031 $output .= selected( 'carried_out', $selected_status, false );
622 - $output .= '>' . __( 'Carried Out', 'propertyhive' ) . '</option>';
1032 + $output .= '>' . esc_html(__( 'Carried Out', 'propertyhive' )) . '</option>';
623 1033
624 1034 $output .= '<option value="won"';
625 1035 $output .= selected( 'won', $selected_status, false );
626 - $output .= '>- ' . __( 'Won', 'propertyhive' ) . '</option>';
1036 + $output .= '>- ' . esc_html(__( 'Won', 'propertyhive' )) . '</option>';
627 1037
628 1038 $output .= '<option value="lost"';
629 1039 $output .= selected( 'lost', $selected_status, false );
630 - $output .= '>- ' . __( 'Lost', 'propertyhive' ) . '</option>';
1040 + $output .= '>- ' . esc_html(__( 'Lost', 'propertyhive' )) . '</option>';
631 1041
632 1042 $output .= '<option value="instructed"';
633 1043 $output .= selected( 'instructed', $selected_status, false );
634 - $output .= '>- ' . __( 'Instructed', 'propertyhive' ) . '</option>';
1044 + $output .= '>- ' . esc_html(__( 'Instructed', 'propertyhive' )) . '</option>';
635 1045
636 1046 $output .= '<option value="cancelled"';
637 1047 $output .= selected( 'cancelled', $selected_status, false );
638 - $output .= '>' . __( 'Cancelled', 'propertyhive' ) . '</option>';
1048 + $output .= '>' . esc_html(__( 'Cancelled', 'propertyhive' )) . '</option>';
639 1049
640 1050 $output .= '</select>';
641 1051
642 1052 return $output;
@@ -642,102 +1052,213 @@
642 1052 return $output;
643 1053 }
644 1054
645 1055 /**
646 - * Show an appraisal attending negotiator filter box
1056 + * Show a viewing filter box
647 1057 */
648 - public function appraisal_attending_negotiator_filter() {
1058 + public function viewing_filters() {
649 1059 global $wp_query;
650 1060
651 - $selected_negotiator_id = isset( $_GET['_negotiator_id']) ? (int)$_GET['_negotiator_id'] : '';
1061 + // Department filtering
1062 + $output = '';
1063 +
1064 + $output .= $this->viewing_status_filter();
1065 + $output .= $this->property_office_filter();
1066 + $output .= $this->negotiator_filter();
1067 + $output .= $this->date_range_filter();
1068 +
1069 + // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Built-in controls escape their text and attributes before this trusted PHP filter adds complete HTML controls.
1070 + echo apply_filters( 'propertyhive_viewing_filters', $output );
1071 + }
1072 +
1073 + /**
1074 + * Show a viewing status filter box
1075 + */
1076 + public function viewing_status_filter() {
1077 + global $wp_query;
1078 +
1079 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only admin list display or query; no state change.
1080 + $requested_value = isset( $_GET['_status'] ) && is_string( $_GET['_status'] ) ? sanitize_text_field( wp_unslash( $_GET['_status'] ) ) : '';
1081 + $selected_status = in_array( $requested_value, array( 'pending', 'confirmed', 'unconfirmed', 'carried_out', 'awaiting_feedback', 'feedback_passed_on', 'feedback_not_passed_on', 'cancelled', 'no_show' ), true ) ? $requested_value : '';
652 1082
653 1083 // Status filtering
654 - $output = '<select name="_negotiator_id" id="dropdown_appraisal_negotiator_id">';
655 -
656 - $output .= '<option value="">Attending Negotiator</option>';
657 - $output .= '<option value="">All Negotiators</option>';
1084 + $output = '<select name="_status" id="dropdown_viewing_status">';
658 1085
659 - $args = array(
660 - 'number' => 9999,
661 - 'orderby' => 'display_name',
662 - 'role__not_in' => array('property_hive_contact')
663 - );
664 - $user_query = new WP_User_Query( $args );
1086 + $output .= '<option value="">' . esc_html__( 'All Statuses', 'propertyhive' ) . '</option>';
665 1087
666 - if ( ! empty( $user_query->results ) )
1088 + $viewing_statuses = ph_get_viewing_statuses();
1089 +
1090 + foreach ( $viewing_statuses as $status => $display_status )
667 1091 {
668 - foreach ( $user_query->results as $user )
669 - {
670 - $output .= '<option value="' . $user->ID . '"';
671 - if ( $user->ID == $selected_negotiator_id )
672 - {
673 - $output .= ' selected';
674 - }
675 - $output .= '>' . $user->display_name . '</option>';
676 - }
1092 + $output .= '<option value="' . esc_attr($status) . '"';
1093 + $output .= selected( $status, $selected_status, false );
1094 + $output .= '>' . esc_html($display_status) . '</option>';
677 1095 }
678 -
1096 +
679 1097 $output .= '</select>';
680 1098
681 1099 return $output;
682 1100 }
683 1101
1102 +
1103 + public function refresh_property_office_filtering( $query ) {
1104 + remove_filter('posts_join', array( $this, 'filter_by_property_office') );
1105 +
1106 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only admin list display or query; no state change.
1107 + if ( ! empty( $_GET['_office_id'] ) && in_array( $query->query['post_type'], array(
1108 + 'viewing',
1109 + 'offer',
1110 + 'sale',
1111 + ))) {
1112 + add_filter('posts_join', array( $this, 'filter_by_property_office' ) );
1113 + };
1114 + }
1115 +
1116 +
1117 + public function filter_by_property_office($query) {
1118 + global $wpdb;
1119 +
1120 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only office filtering; no state change.
1121 + $office_id = isset( $_GET['_office_id'] ) && is_scalar( $_GET['_office_id'] ) ? absint( $_GET['_office_id'] ) : 0;
1122 +
1123 + return $query . '
1124 + INNER JOIN ' . $wpdb->postmeta . ' AS property_meta ON property_meta.post_id = ' . $wpdb->posts . '.ID AND property_meta.meta_key = "_property_id"
1125 + INNER JOIN ' . $wpdb->postmeta . ' AS property_office_meta ON property_office_meta.post_id = property_meta.meta_value AND property_office_meta.meta_key = "_office_id"
1126 + AND property_office_meta.meta_value = ' . $office_id;
1127 + }
1128 +
684 1129 /**
685 - * Show a viewing filter box
1130 + * Show an offer filter box
686 1131 */
687 - public function viewing_filters() {
1132 + public function offer_filters() {
688 1133 global $wp_query;
689 1134
690 - // Department filtering
691 1135 $output = '';
692 1136
693 - $output .= $this->viewing_status_filter();
694 - $output .= $this->viewing_attending_negotiator_filter();
1137 + $output .= $this->offer_status_filter();
1138 + $output .= $this->property_office_filter();
1139 + $output .= $this->date_range_filter();
695 1140
696 - echo apply_filters( 'propertyhive_viewing_filters', $output );
1141 + // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Built-in controls escape their text and attributes before this trusted PHP filter adds complete HTML controls.
1142 + echo apply_filters( 'propertyhive_offer_filters', $output );
697 1143 }
698 1144
699 1145 /**
700 - * Show a viewing status filter box
1146 + * Show an offer status filter box
701 1147 */
702 - public function viewing_status_filter() {
1148 + public function offer_status_filter() {
703 1149 global $wp_query;
704 1150
705 - $selected_status = isset( $_GET['_status'] ) && in_array( $_GET['_status'], array( 'pending', 'confirmed', 'unconfirmed', 'carried_out', 'feedback_passed_on', 'feedback_not_passed_on', 'cancelled' ) ) ? $_GET['_status'] : '';
1151 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only admin list display or query; no state change.
1152 + $requested_value = isset( $_GET['_status'] ) && is_string( $_GET['_status'] ) ? sanitize_text_field( wp_unslash( $_GET['_status'] ) ) : '';
1153 + $selected_status = in_array( $requested_value, array( 'pending', 'accepted', 'declined' ), true ) ? $requested_value : '';
706 1154
707 1155 // Status filtering
708 - $output = '<select name="_status" id="dropdown_viewing_status">';
1156 + $output = '<select name="_status" id="dropdown_offer_status">';
1157 +
1158 + $output .= '<option value="">' . esc_html(__( 'All Statuses', 'propertyhive' )) . '</option>';
1159 +
1160 + $offer_statuses = ph_get_offer_statuses();
1161 +
1162 + foreach ( $offer_statuses as $status => $display_status )
1163 + {
1164 + $output .= '<option value="' . esc_attr($status) . '"';
1165 + $output .= selected( $status, $selected_status, false );
1166 + $output .= '>' . esc_html($display_status) . '</option>';
1167 + }
1168 +
1169 + $output .= '</select>';
1170 +
1171 + return $output;
1172 + }
1173 +
1174 + /**
1175 + * Show an sale filter box
1176 + */
1177 + public function sale_filters() {
1178 + global $wp_query;
1179 +
1180 + $output = '';
1181 +
1182 + $output .= $this->sale_status_filter();
1183 + $output .= $this->property_office_filter();
1184 + $output .= $this->date_range_filter();
1185 +
1186 + // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Built-in controls escape their text and attributes before this trusted PHP filter adds complete HTML controls.
1187 + echo apply_filters( 'propertyhive_sale_filters', $output );
1188 + }
1189 +
1190 + /**
1191 + * Show an sale status filter box
1192 + */
1193 + public function sale_status_filter() {
1194 + global $wp_query;
1195 +
1196 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only admin list display or query; no state change.
1197 + $requested_value = isset( $_GET['_status'] ) && is_string( $_GET['_status'] ) ? sanitize_text_field( wp_unslash( $_GET['_status'] ) ) : '';
1198 + $selected_status = in_array( $requested_value, array( 'current', 'exchanged', 'completed', 'fallen_through' ), true ) ? $requested_value : '';
1199 +
1200 + // Status filtering
1201 + $output = '<select name="_status" id="dropdown_sale_status">';
709 1202
710 - $output .= '<option value="">All Statuses</option>';
1203 + $output .= '<option value="">' . esc_html__( 'All Statuses', 'propertyhive' ) . '</option>';
711 1204
712 - $output .= '<option value="pending"';
713 - $output .= selected( 'pending', $selected_status, false );
714 - $output .= '>' . __( 'Pending', 'propertyhive' ) . '</option>';
1205 + $sale_statuses = ph_get_sale_statuses();
715 1206
716 - $output .= '<option value="confirmed"';
717 - $output .= selected( 'confirmed', $selected_status, false );
718 - $output .= '>- ' . __( 'Confirmed', 'propertyhive' ) . '</option>';
1207 + foreach ( $sale_statuses as $status => $display_status )
1208 + {
1209 + $output .= '<option value="' . esc_attr($status) . '"';
1210 + $output .= selected( $status, $selected_status, false );
1211 + $output .= '>' . esc_html($display_status) . '</option>';
1212 + }
1213 +
1214 + $output .= '</select>';
719 1215
720 - $output .= '<option value="unconfirmed"';
721 - $output .= selected( 'unconfirmed', $selected_status, false );
722 - $output .= '>- ' . __( 'Awaiting Confirmation', 'propertyhive' ) . '</option>';
1216 + return $output;
1217 + }
723 1218
724 - $output .= '<option value="carried_out"';
725 - $output .= selected( 'carried_out', $selected_status, false );
726 - $output .= '>' . __( 'Carried Out', 'propertyhive' ) . '</option>';
1219 + /**
1220 + * Show an tenancy filter box
1221 + */
1222 + public function tenancy_filters() {
1223 + global $wp_query;
727 1224
728 - $output .= '<option value="feedback_passed_on"';
729 - $output .= selected( 'feedback_passed_on', $selected_status, false );
730 - $output .= '>- ' . __( 'Feedback Passed On', 'propertyhive' ) . '</option>';
1225 + $output = '';
731 1226
732 - $output .= '<option value="feedback_not_passed_on"';
733 - $output .= selected( 'feedback_not_passed_on', $selected_status, false );
734 - $output .= '>- ' . __( 'Feedback Not Passed On', 'propertyhive' ) . '</option>';
1227 + $output .= $this->tenancy_status_filter();
1228 + $output .= $this->tenancy_management_type_filter();
735 1229
736 - $output .= '<option value="cancelled"';
737 - $output .= selected( 'cancelled', $selected_status, false );
738 - $output .= '>' . __( 'Cancelled', 'propertyhive' ) . '</option>';
739 -
1230 + // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Built-in controls escape their text and attributes before this trusted PHP filter adds complete HTML controls.
1231 + echo apply_filters( 'propertyhive_tenancy_filters', $output );
1232 + }
1233 +
1234 + /**
1235 + * Show an tenancy status filter box
1236 + */
1237 + public function tenancy_status_filter() {
1238 + global $wp_query;
1239 +
1240 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only admin list display or query; no state change.
1241 + $requested_value = isset( $_GET['_status'] ) && is_string( $_GET['_status'] ) ? sanitize_text_field( wp_unslash( $_GET['_status'] ) ) : '';
1242 + $selected_status = in_array( $requested_value, array( 'pending', 'current', 'finished'), true ) ? $requested_value : '';
1243 +
1244 + // Status filtering
1245 + $output = '<select name="_status" id="dropdown_tenancy_status">';
1246 +
1247 + $output .= '<option value="">' . esc_html(__( 'All Statuses', 'propertyhive' )) . '</option>';
1248 +
1249 + $output .= '<option value="pending"';
1250 + $output .= selected( 'pending', $selected_status, false );
1251 + $output .= '>' . esc_html(__( 'Pending', 'propertyhive' )) . '</option>';
1252 +
1253 + $output .= '<option value="current"';
1254 + $output .= selected( 'current', $selected_status, false );
1255 + $output .= '> ' . esc_html(__( 'Current', 'propertyhive' )) . '</option>';
1256 +
1257 + $output .= '<option value="finished"';
1258 + $output .= selected( 'finished', $selected_status, false );
1259 + $output .= '> ' . esc_html(__( 'Finished', 'propertyhive' )) . '</option>';
1260 +
740 1261 $output .= '</select>';
741 1262
742 1263 return $output;
743 1264 }
@@ -742,45 +1263,122 @@
742 1263 return $output;
743 1264 }
744 1265
745 1266 /**
746 - * Show a viewing attending negotiator filter box
1267 + * Show an tenancy management type filter box
747 1268 */
748 - public function viewing_attending_negotiator_filter() {
1269 + public function tenancy_management_type_filter() {
749 1270 global $wp_query;
750 1271
751 - $selected_negotiator_id = isset( $_GET['_negotiator_id']) ? (int)$_GET['_negotiator_id'] : '';
752 -
1272 + $management_types = apply_filters( 'propertyhive_tenancy_management_types', array(
1273 + 'let_only' => 'Let Only',
1274 + 'fully_managed' => 'Fully Managed'
1275 + ) );
1276 +
1277 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only admin list display or query; no state change.
1278 + $requested_value = isset( $_GET['_management_type'] ) && is_string( $_GET['_management_type'] ) ? sanitize_text_field( wp_unslash( $_GET['_management_type'] ) ) : '';
1279 + $selected_management_type = array_key_exists( $requested_value, $management_types ) ? $requested_value : '';
1280 +
753 1281 // Status filtering
754 - $output = '<select name="_negotiator_id" id="dropdown_viewing_negotiator_id">';
755 -
756 - $output .= '<option value="">Attending Negotiator</option>';
757 - $output .= '<option value="">All Negotiators</option>';
1282 + $output = '<select name="_management_type" id="dropdown_tenancy_management_type">';
758 1283
759 - $args = array(
760 - 'number' => 9999,
761 - 'orderby' => 'display_name',
762 - 'role__not_in' => array('property_hive_contact')
763 - );
764 - $user_query = new WP_User_Query( $args );
1284 + $output .= '<option value="">' . esc_html(__( 'All Management Types', 'propertyhive' )) . '</option>';
765 1285
766 - if ( ! empty( $user_query->results ) )
1286 + foreach ( $management_types as $key => $value )
767 1287 {
768 - foreach ( $user_query->results as $user )
769 - {
770 - $output .= '<option value="' . $user->ID . '"';
771 - if ( $user->ID == $selected_negotiator_id )
772 - {
773 - $output .= ' selected';
774 - }
775 - $output .= '>' . $user->display_name . '</option>';
776 - }
1288 + $output .= '<option value="' . esc_attr($key) . '"';
1289 + $output .= selected( $key, $selected_management_type, false );
1290 + $output .= '>' . esc_html( $value ) . '</option>';
777 1291 }
778 -
1292 +
779 1293 $output .= '</select>';
780 1294
781 1295 return $output;
782 1296 }
1297 +
1298 + public function key_date_filters() {
1299 + global $wp_query;
1300 +
1301 + $output = '';
1302 +
1303 + $output .= $this->key_date_type_filter();
1304 + $output .= $this->key_date_status_filter();
1305 + $output .= $this->date_range_filter();
1306 +
1307 + // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Built-in controls escape their text and attributes before this trusted PHP filter adds complete HTML controls.
1308 + echo apply_filters( 'propertyhive_tenancy_filters', $output );
1309 + }
1310 +
1311 + public function key_date_type_filter() {
1312 +
1313 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only admin list display or query; no state change.
1314 + $selected_value = ! empty($_GET['_key_date_type_id']) ? (int)$_GET['_key_date_type_id'] : '';
1315 + $terms = get_terms( array_merge( wp_parse_args( array(
1316 + 'hide_empty' => false,
1317 + 'parent' => 0
1318 + ) ), array( 'taxonomy' => 'management_key_date_type' ) ) );
1319 +
1320 + $output = '<select name="_key_date_type_id">';
1321 + $output .= '<option value="">' . esc_html(__( 'All Types', 'propertyhive' )) . '</option>';
1322 +
1323 + if ( !empty( $terms ) && !is_wp_error( $terms ) )
1324 + {
1325 + foreach ($terms as $term)
1326 + {
1327 + $output .= '<option value="' . esc_attr($term->term_id) . '"';
1328 + $output .= selected($term->term_id, $selected_value, false );
1329 + $output .= '>' . esc_html($term->name) . '</option>';
1330 + }
1331 + }
1332 +
1333 + $output .= '</select>';
1334 +
1335 + return $output;
1336 + }
1337 +
1338 +
1339 + public function key_date_status_filter() {
1340 +
1341 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only admin list display or query; no state change.
1342 + $requested_value = isset( $_GET['status'] ) && is_string( $_GET['status'] ) ? sanitize_text_field( wp_unslash( $_GET['status'] ) ) : '';
1343 + $selected_status = in_array( $requested_value, array( 'upcoming_and_overdue', 'overdue', 'booked', 'complete', 'pending', 'on_hold', 'cancelled'), true ) ? $requested_value : '';
1344 +
1345 + $output = '<select name="status" id="dropdown_key_date_status">';
1346 +
1347 + $output .= '<option value="">' . esc_html(__( 'All Statuses', 'propertyhive' )) . '</option>';
1348 +
1349 + $output .= '<option value="upcoming_and_overdue"';
1350 + $output .= selected( 'upcoming_and_overdue', $selected_status, false );
1351 + $output .= '>' . esc_html(__( 'Upcoming & Overdue', 'propertyhive' )) . '</option>';
1352 +
1353 + $output .= '<option value="overdue"';
1354 + $output .= selected( 'overdue', $selected_status, false );
1355 + $output .= '>' . esc_html(__( 'Overdue', 'propertyhive' )) . '</option>';
1356 +
1357 + $output .= '<option value="booked"';
1358 + $output .= selected( 'booked', $selected_status, false );
1359 + $output .= '> ' . esc_html(__( 'Booked', 'propertyhive' )) . '</option>';
1360 +
1361 + $output .= '<option value="complete"';
1362 + $output .= selected( 'complete', $selected_status, false );
1363 + $output .= '> ' . esc_html(__( 'Complete', 'propertyhive' )) . '</option>';
1364 +
1365 + $output .= '<option value="pending"';
1366 + $output .= selected( 'pending', $selected_status, false );
1367 + $output .= '> ' . esc_html(__( 'Pending', 'propertyhive' )) . '</option>';
1368 +
1369 + $output .= '<option value="on_hold"';
1370 + $output .= selected( 'on_hold', $selected_status, false );
1371 + $output .= '> ' . esc_html(__( 'On Hold', 'propertyhive' )) . '</option>';
1372 +
1373 + $output .= '<option value="cancelled"';
1374 + $output .= selected( 'cancelled', $selected_status, false );
1375 + $output .= '> ' . esc_html(__( 'Cancelled', 'propertyhive' )) . '</option>';
1376 +
1377 + $output .= '</select>';
1378 +
1379 + return $output;
1380 + }
783 1381
784 1382 /**
785 1383 * Filters and sorting handler
786 1384 * @param array $vars
@@ -788,50 +1386,71 @@
788 1386 */
789 1387 public function request_query( $vars ) {
790 1388 global $typenow, $wp_query;
791 1389
1390 + // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- These hooks add status/department/taxonomy/date filters to the main admin list query. WordPress supplies the list query’s pagination; values are sanitized or selected from fixed post-type/date keys. These are request_query/filter_by_date_range values consumed by the core list table query rather than independent nopaging loops. The date meta key is chosen by post type.
792 1391 if ( !isset($vars['meta_query']) ) { $vars['meta_query'] = array(); }
1392 + // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query -- These hooks add status/department/taxonomy/date filters to the main admin list query. WordPress supplies the list query’s pagination; values are sanitized or selected from fixed post-type/date keys. These are request_query/filter_by_date_range values consumed by the core list table query rather than independent nopaging loops. The date meta key is chosen by post type.
793 1393 if ( !isset($vars['tax_query']) ) { $vars['tax_query'] = array(); }
794 1394
1395 + $department = $this->get_admin_query_value( '_department' );
1396 + $marketing = $this->get_admin_query_value( '_marketing' );
1397 + $contact_type = $this->get_admin_query_value( '_contact_type' );
1398 + $status = $this->get_admin_query_value( '_status' );
1399 + $source = $this->get_admin_query_value( '_source' );
1400 + $management_type = $this->get_admin_query_value( '_management_type' );
1401 + $key_date_status = $this->get_admin_query_value( 'status' );
1402 +
795 1403 if ( 'property' === $typenow )
796 1404 {
797 - if ( ! empty( $_GET['_department'] ) ) {
1405 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only admin list display or query; no state change.
1406 + if ( ! empty( $department ) ) {
798 1407 $vars['meta_query'][] = array(
799 1408 'key' => '_department',
800 - 'value' => sanitize_text_field( $_GET['_department'] ),
1409 + 'value' => $department,
801 1410 );
802 1411 }
1412 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only admin list display or query; no state change.
803 1413 if ( ! empty( $_GET['_office_id'] ) ) {
804 1414 $vars['meta_query'][] = array(
805 1415 'key' => '_office_id',
1416 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only admin list display or query; no state change.
806 1417 'value' => (int)$_GET['_office_id'],
807 1418 );
808 1419 }
1420 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only admin list display or query; no state change.
809 1421 if ( ! empty( $_GET['_negotiator_id'] ) ) {
810 1422 $vars['meta_query'][] = array(
811 1423 'key' => '_negotiator_id',
1424 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only admin list display or query; no state change.
812 1425 'value' => (int)$_GET['_negotiator_id'],
813 1426 );
814 1427 }
1428 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only admin list display or query; no state change.
815 1429 if ( ! empty( $_GET['_location_id'] ) ) {
816 1430 $vars['tax_query'][] = array(
817 1431 'taxonomy' => 'location',
1432 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only admin list display or query; no state change.
818 1433 'terms' => ( (is_array($_GET['_location_id'])) ? (int)$_GET['_location_id'] : array( (int)$_GET['_location_id'] ) )
819 1434 );
820 1435 }
1436 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only admin list display or query; no state change.
821 1437 if ( ! empty( $_GET['_availability_id'] ) ) {
822 1438 $vars['tax_query'][] = array(
823 1439 'taxonomy' => 'availability',
1440 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only admin list display or query; no state change.
824 1441 'terms' => ( (is_array($_GET['_availability_id'])) ? (int)$_GET['_availability_id'] : array( (int)$_GET['_availability_id'] ) )
825 1442 );
826 1443 }
827 - if ( ! empty( $_GET['_marketing'] ) && $_GET['_marketing'] == 'on_market' ) {
1444 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only admin list display or query; no state change.
1445 + if ( 'on_market' === $marketing ) {
828 1446 $vars['meta_query'][] = array(
829 1447 'key' => '_on_market',
830 1448 'value' => 'yes',
831 1449 );
832 1450 }
833 - if ( ! empty( $_GET['_marketing'] ) && $_GET['_marketing'] == 'off_market' ) {
1451 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only admin list display or query; no state change.
1452 + if ( 'off_market' === $marketing ) {
834 1453 $vars['meta_query'][] = array(
835 1454 'key' => '_on_market',
836 1455 'value' => 'yes',
837 1456 'compare' => '!=',
@@ -836,16 +1455,18 @@
836 1455 'value' => 'yes',
837 1456 'compare' => '!=',
838 1457 );
839 1458 }
840 - if ( ! empty( $_GET['_marketing'] ) && $_GET['_marketing'] == 'featured' ) {
1459 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only admin list display or query; no state change.
1460 + if ( 'featured' === $marketing ) {
841 1461 $vars['meta_query'][] = array(
842 1462 'key' => '_featured',
843 1463 'value' => 'yes',
844 1464 );
845 - }
846 - if ( ! empty( $_GET['_marketing'] ) && substr($_GET['_marketing'], 0, 15) == 'marketing_flag_' ) {
847 - $marketing_flag_id = sanitize_text_field( str_replace("marketing_flag_", "", $_GET['_marketing']) );
1465 + }
1466 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only admin list display or query; no state change.
1467 + if ( 0 === strpos( $marketing, 'marketing_flag_' ) ) {
1468 + $marketing_flag_id = str_replace( 'marketing_flag_', '', $marketing );
848 1469 $vars['tax_query'][] = array(
849 1470 'taxonomy' => 'marketing_flag',
850 1471 'terms' => ( (is_array($marketing_flag_id)) ? $marketing_flag_id : array( $marketing_flag_id ) )
851 1472 );
@@ -852,35 +1473,81 @@
852 1473 }
853 1474 }
854 1475 elseif ( 'contact' === $typenow )
855 1476 {
856 - if ( ! empty( $_GET['_contact_type'] ) ) {
1477 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only admin list display or query; no state change.
1478 + if ( ! empty( $contact_type ) )
1479 + {
1480 + if ( $contact_type == 'hotapplicant' )
1481 + {
1482 + $contact_type = 'applicant';
1483 +
1484 + $vars['meta_query'][] = array(
1485 + 'key' => '_hot_applicant',
1486 + 'value' => 'yes',
1487 + );
1488 + }
857 1489 $vars['meta_query'][] = array(
858 1490 'key' => '_contact_types',
859 - 'value' => sanitize_text_field( $_GET['_contact_type'] ),
1491 + 'value' => $contact_type,
860 1492 'compare' => 'LIKE'
861 1493 );
862 1494 }
1495 +
1496 + $vars = $this->filter_by_date_range($vars, 'date_query');
863 1497 }
864 - elseif ( 'enquiry' === $typenow )
1498 + elseif ( 'enquiry' === $typenow )
865 1499 {
866 - if ( ! empty( $_GET['_status'] ) ) {
1500 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only admin list display or query; no state change.
1501 + if ( ! empty( $status ) && $status != 'all' ) {
1502 +
867 1503 $vars['meta_query'][] = array(
868 1504 'key' => '_status',
869 - 'value' => sanitize_text_field( $_GET['_status'] ),
1505 + 'value' => $status,
870 1506 );
871 1507 }
872 - if ( ! empty( $_GET['_source'] ) ) {
1508 + else
1509 + {
1510 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only admin list display or query; no state change.
1511 + if ( empty( $status ) )
1512 + {
1513 + $vars['meta_query'][] = array(
1514 + 'key' => '_status',
1515 + 'value' => 'open',
1516 + );
1517 + }
1518 + }
1519 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only admin list display or query; no state change.
1520 + if ( ! empty( $source ) ) {
873 1521 $vars['meta_query'][] = array(
874 1522 'key' => '_source',
875 - 'value' => sanitize_text_field( $_GET['_source'] ),
1523 + 'value' => $source,
876 1524 );
877 1525 }
1526 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only admin list display or query; no state change.
1527 + if ( ! empty( $_GET['_office_id'] ) ) {
1528 + $vars['meta_query'][] = array(
1529 + 'key' => '_office_id',
1530 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only admin list display or query; no state change.
1531 + 'value' => (int)$_GET['_office_id'],
1532 + );
1533 + }
1534 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only admin list display or query; no state change.
1535 + if ( ! empty( $_GET['_negotiator_id'] ) ) {
1536 + $vars['meta_query'][] = array(
1537 + 'key' => '_negotiator_id',
1538 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only admin list display or query; no state change.
1539 + 'value' => (int)$_GET['_negotiator_id'],
1540 + );
1541 + }
1542 +
1543 + $vars = $this->filter_by_date_range($vars, 'date_query');
878 1544 }
879 - elseif ( 'appraisal' === $typenow )
1545 + elseif ( 'appraisal' === $typenow )
880 1546 {
881 - if ( ! empty( $_GET['_status'] ) ) {
882 - switch ( sanitize_text_field( $_GET['_status'] ) )
1547 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only admin list display or query; no state change.
1548 + if ( ! empty( $status ) ) {
1549 + switch ( $status )
883 1550 {
884 1551 case "confirmed":
885 1552 {
886 1553 $vars['meta_query'][] = array(
@@ -908,100 +1575,203 @@
908 1575 default:
909 1576 {
910 1577 $vars['meta_query'][] = array(
911 1578 'key' => '_status',
912 - 'value' => sanitize_text_field( $_GET['_status'] ),
1579 + 'value' => $status,
913 1580 );
914 1581 }
915 1582 }
916 1583 }
1584 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only admin list display or query; no state change.
917 1585 if ( ! empty( $_GET['_negotiator_id'] ) )
918 1586 {
919 1587 $vars['meta_query'][] = array(
920 1588 'key' => '_negotiator_id',
1589 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only admin list display or query; no state change.
921 1590 'value' => (int)$_GET['_negotiator_id'],
922 1591 );
923 1592 }
1593 +
1594 + $vars = $this->filter_by_date_range($vars);
924 1595 }
925 1596 elseif ( 'viewing' === $typenow )
926 1597 {
927 - if ( ! empty( $_GET['_status'] ) ) {
928 - switch ( sanitize_text_field( $_GET['_status'] ) )
1598 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only admin list display or query; no state change.
1599 + if ( ! empty( $status ) ) {
1600 +
1601 + // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query,WordPress.Security.NonceVerification.Recommended -- Read-only status filtering of the paginated core viewing list uses the existing viewing metadata schema; no state change.
1602 + $vars['meta_query'] = add_viewing_status_meta_query( $vars['meta_query'], $status );
1603 +
1604 + }
1605 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only admin list display or query; no state change.
1606 + if ( ! empty( $_GET['_negotiator_id'] ) )
1607 + {
1608 + $vars['meta_query'][] = array(
1609 + 'key' => '_negotiator_id',
1610 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only admin list display or query; no state change.
1611 + 'value' => (int)$_GET['_negotiator_id'],
1612 + );
1613 + }
1614 +
1615 + $vars = $this->filter_by_date_range($vars);
1616 + }
1617 + elseif ( 'offer' === $typenow )
1618 + {
1619 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only admin list display or query; no state change.
1620 + if ( ! empty( $status ) ) {
1621 + $vars['meta_query'][] = array(
1622 + 'key' => '_status',
1623 + 'value' => $status,
1624 + );
1625 + }
1626 +
1627 + $vars = $this->filter_by_date_range($vars, '_offer_date_time');
1628 + }
1629 + elseif ( 'sale' === $typenow )
1630 + {
1631 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only admin list display or query; no state change.
1632 + if ( ! empty( $status ) ) {
1633 + $vars['meta_query'][] = array(
1634 + 'key' => '_status',
1635 + 'value' => $status,
1636 + );
1637 + }
1638 +
1639 + $vars = $this->filter_by_date_range($vars, '_sale_date_time');
1640 + }
1641 + elseif ( 'tenancy' === $typenow )
1642 + {
1643 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only admin list display or query; no state change.
1644 + if ( ! empty( $status ) )
1645 + {
1646 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only admin list display or query; no state change.
1647 + switch ( $status )
929 1648 {
930 - case "confirmed":
931 - {
1649 + case 'pending' :
932 1650 $vars['meta_query'][] = array(
933 - 'key' => '_status',
934 - 'value' => 'pending',
1651 + 'key' => '_start_date',
1652 + 'value' => gmdate('Y-m-d'),
1653 + 'type' => 'date',
1654 + 'compare' => '>',
935 1655 );
1656 + break;
1657 +
1658 + case 'current' :
936 1659 $vars['meta_query'][] = array(
937 - 'key' => '_all_confirmed',
938 - 'value' => 'yes',
1660 + 'relation' => 'OR',
1661 + array(
1662 + array(
1663 + 'key' => '_start_date',
1664 + 'value' => gmdate('Y-m-d'),
1665 + 'type' => 'date',
1666 + 'compare' => '<=',
1667 + ),
1668 + array(
1669 + 'key' => '_end_date',
1670 + 'value' => gmdate('Y-m-d'),
1671 + 'type' => 'date',
1672 + 'compare' => '>=',
1673 + )
1674 + ),
1675 + array(
1676 + array(
1677 + 'key' => '_start_date',
1678 + 'value' => gmdate('Y-m-d'),
1679 + 'type' => 'date',
1680 + 'compare' => '<=',
1681 + ),
1682 + array(
1683 + 'key' => '_end_date',
1684 + 'value' => '',
1685 + 'compare' => '=',
1686 + )
1687 + )
939 1688 );
940 1689 break;
941 - }
942 - case "unconfirmed":
943 - {
1690 +
1691 + case 'finished':
944 1692 $vars['meta_query'][] = array(
945 - 'key' => '_status',
946 - 'value' => 'pending',
1693 + 'key' => '_end_date',
1694 + 'value' => gmdate('Y-m-d'),
1695 + 'type' => 'date',
1696 + 'compare' => '<',
947 1697 );
1698 + break;
1699 + }
1700 + }
1701 +
1702 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only admin list display or query; no state change.
1703 + if ( ! empty( $management_type ) ) {
1704 + $vars['meta_query'][] = array(
1705 + 'key' => '_management_type',
1706 + 'value' => $management_type,
1707 + );
1708 + }
1709 + }
1710 + elseif ( 'key_date' === $typenow )
1711 + {
1712 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only admin list display or query; no state change.
1713 + if ( ! empty( $key_date_status ) ) {
1714 +
1715 + $value = $key_date_status;
1716 +
1717 + switch ($value) {
1718 + case 'booked':
1719 + case 'complete':
1720 + case 'on_hold':
1721 + case 'cancelled':
948 1722 $vars['meta_query'][] = array(
949 - 'key' => '_all_confirmed',
950 - 'value' => '',
1723 + 'key' => '_key_date_status',
1724 + 'value' => $value,
951 1725 );
952 1726 break;
953 - }
954 - case "feedback_passed_on":
955 - {
1727 + case 'pending':
956 1728 $vars['meta_query'][] = array(
957 - 'key' => '_status',
958 - 'value' => 'carried_out',
1729 + 'key' => '_key_date_status',
1730 + 'value' => 'pending',
959 1731 );
1732 + break;
1733 + case 'overdue':
960 1734 $vars['meta_query'][] = array(
961 - 'key' => '_feedback_status',
962 - 'value' => array('interested', 'not_interested'),
1735 + 'key' => '_key_date_status',
1736 + 'value' => array('pending', 'booked'),
963 1737 'compare' => 'IN'
964 1738 );
965 1739 $vars['meta_query'][] = array(
966 - 'key' => '_feedback_passed_on',
967 - 'value' => 'yes',
1740 + 'key' => '_date_due',
1741 + 'value' => gmdate("Y-m-d"),
1742 + 'type' => 'date',
1743 + 'compare' => '<',
968 1744 );
969 1745 break;
970 - }
971 - case "feedback_not_passed_on":
972 - {
973 - $vars['meta_query'][] = array(
974 - 'key' => '_status',
975 - 'value' => 'carried_out',
976 - );
1746 + case 'upcoming_and_overdue':
977 1747 $vars['meta_query'][] = array(
978 - 'key' => '_feedback_status',
979 - 'value' => array('interested', 'not_interested'),
1748 + 'key' => '_key_date_status',
1749 + 'value' => array('pending', 'booked'),
980 1750 'compare' => 'IN'
981 1751 );
1752 + $upcoming_threshold = new DateTime('+ ' . apply_filters( 'propertyhive_key_date_upcoming_days', 7 ) . ' DAYS');
982 1753 $vars['meta_query'][] = array(
983 - 'key' => '_feedback_passed_on',
984 - 'value' => '',
1754 + 'key' => '_date_due',
1755 + 'value' => $upcoming_threshold->format('Y-m-d'),
1756 + 'type' => 'date',
1757 + 'compare' => '<=',
985 1758 );
986 1759 break;
987 - }
988 - default:
989 - {
990 - $vars['meta_query'][] = array(
991 - 'key' => '_status',
992 - 'value' => sanitize_text_field( $_GET['_status'] ),
993 - );
994 - }
995 1760 }
996 1761 }
997 - if ( ! empty( $_GET['_negotiator_id'] ) )
1762 +
1763 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only admin list display or query; no state change.
1764 + if ( !empty( $_GET['_key_date_type_id'] ) )
998 1765 {
999 1766 $vars['meta_query'][] = array(
1000 - 'key' => '_negotiator_id',
1001 - 'value' => (int)$_GET['_negotiator_id'],
1767 + 'key' => '_key_date_type_id',
1768 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only admin list display or query; no state change.
1769 + 'value' => (int)$_GET['_key_date_type_id'],
1002 1770 );
1003 1771 }
1772 +
1773 + $vars = $this->filter_by_date_range($vars, '_date_due');
1004 1774 }
1005 1775
1006 1776 $vars = apply_filters( 'propertyhive_property_filter_query', $vars, $typenow );
1007 1777
@@ -1007,8 +1777,249 @@
1007 1777
1008 1778 return $vars;
1009 1779 }
1010 1780
1781 + private function filter_by_date_range($vars, $meta_key = '_start_date_time')
1782 + {
1783 + $date_range_label = $this->get_admin_query_value( '_date_range_label' );
1784 + $date_range_from = $this->get_admin_query_value( '_date_range_from' );
1785 + $date_range_to = $this->get_admin_query_value( '_date_range_to' );
1786 +
1787 + if (
1788 + ! empty( $date_range_label )
1789 + && ! empty( $date_range_from )
1790 + && ! empty( $date_range_to )
1791 + && $date_range_label !== 'Any Time'
1792 + && DateTime::createFromFormat('Y-m-d', $date_range_from) !== false
1793 + && DateTime::createFromFormat('Y-m-d', $date_range_to) !== false
1794 + )
1795 + {
1796 + if ( $meta_key == 'date_query' )
1797 + {
1798 + $vars['date_query'] = array(
1799 + 'after' => $date_range_from . ' 00:00:00',
1800 + 'before' => $date_range_to . ' 23:59:59',
1801 + );
1802 + }
1803 + else
1804 + {
1805 + // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- Add validated date boundaries using the fixed date key selected for this paginated admin post-type list.
1806 + $vars['meta_query'] = array_merge($vars['meta_query'], array (
1807 + array(
1808 + 'key' => $meta_key,
1809 + 'value' => $date_range_from,
1810 + 'type' => 'date',
1811 + 'compare' => '>='
1812 + ),
1813 + array(
1814 + 'key' => $meta_key,
1815 + 'value' => $date_range_to,
1816 + 'type' => 'date',
1817 + 'compare' => '<='
1818 + ),
1819 + ));
1820 + }
1821 + }
1822 +
1823 + return $vars;
1824 + }
1825 +
1826 + public function posts_join( $join, $q ) {
1827 + global $typenow, $wp_query, $wpdb;
1828 +
1829 + if ( !$q->is_main_query() )
1830 + return $join;
1831 +
1832 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only admin list display or query; no state change.
1833 + $search = isset( $_GET['s'] ) && is_string( $_GET['s'] ) ? sanitize_text_field( wp_unslash( $_GET['s'] ) ) : '';
1834 + if ( $search === '' ) {
1835 + return $join;
1836 + }
1837 +
1838 + if ( 'property' === $typenow )
1839 + {
1840 + $join .= "
1841 +LEFT JOIN " . $wpdb->postmeta . " AS ph_property_filter_meta_address_concatenated ON " . $wpdb->posts . ".ID = ph_property_filter_meta_address_concatenated.post_id AND ph_property_filter_meta_address_concatenated.meta_key = '_address_concatenated'
1842 +LEFT JOIN " . $wpdb->postmeta . " AS ph_property_filter_meta_reference_number ON " . $wpdb->posts . ".ID = ph_property_filter_meta_reference_number.post_id AND ph_property_filter_meta_reference_number.meta_key = '_reference_number'
1843 +LEFT JOIN " . $wpdb->postmeta . " AS ph_property_filter_meta_owner_details ON " . $wpdb->posts . ".ID = ph_property_filter_meta_owner_details.post_id AND ph_property_filter_meta_owner_details.meta_key = '_owner_details'
1844 +";
1845 + }
1846 + elseif ( 'contact' === $typenow )
1847 + {
1848 + $phone_number = '';
1849 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only admin list display or query; no state change.
1850 + if ( is_numeric(substr($search, 0, 1)) )
1851 + {
1852 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only admin list display or query; no state change.
1853 + $phone_number = preg_replace( "/[^0-9,]/", "", $search );
1854 + }
1855 +
1856 + $join .= "
1857 +LEFT JOIN " . $wpdb->postmeta . " AS ph_contact_filter_meta_address_concatenated ON " . $wpdb->posts . ".ID = ph_contact_filter_meta_address_concatenated.post_id AND ph_contact_filter_meta_address_concatenated.meta_key = '_address_concatenated'
1858 +LEFT JOIN " . $wpdb->postmeta . " AS ph_contact_filter_meta_email_address ON " . $wpdb->posts . ".ID = ph_contact_filter_meta_email_address.post_id AND ph_contact_filter_meta_email_address.meta_key = '_email_address' ";
1859 +
1860 + if ( $phone_number != '' )
1861 + {
1862 + $join .= " LEFT JOIN " . $wpdb->postmeta . " AS ph_contact_filter_meta_telephone_number ON " . $wpdb->posts . ".ID = ph_contact_filter_meta_telephone_number.post_id AND ph_contact_filter_meta_telephone_number.meta_key = '_telephone_number_clean'
1863 + ";
1864 + }
1865 + }
1866 + elseif ( 'appraisal' === $typenow )
1867 + {
1868 + $join .= "
1869 +LEFT JOIN " . $wpdb->postmeta . " AS ph_appraisal_filter_meta_name_number ON " . $wpdb->posts . ".ID = ph_appraisal_filter_meta_name_number.post_id AND ph_appraisal_filter_meta_name_number.meta_key = '_address_name_number'
1870 +LEFT JOIN " . $wpdb->postmeta . " AS ph_appraisal_filter_meta_street ON " . $wpdb->posts . ".ID = ph_appraisal_filter_meta_street.post_id AND ph_appraisal_filter_meta_street.meta_key = '_address_street'
1871 +LEFT JOIN " . $wpdb->postmeta . " AS ph_appraisal_filter_meta_2 ON " . $wpdb->posts . ".ID = ph_appraisal_filter_meta_2.post_id AND ph_appraisal_filter_meta_2.meta_key = '_address_two'
1872 +LEFT JOIN " . $wpdb->postmeta . " AS ph_appraisal_filter_meta_3 ON " . $wpdb->posts . ".ID = ph_appraisal_filter_meta_3.post_id AND ph_appraisal_filter_meta_3.meta_key = '_address_three'
1873 +LEFT JOIN " . $wpdb->postmeta . " AS ph_appraisal_filter_meta_4 ON " . $wpdb->posts . ".ID = ph_appraisal_filter_meta_4.post_id AND ph_appraisal_filter_meta_4.meta_key = '_address_four'
1874 +LEFT JOIN " . $wpdb->postmeta . " AS ph_appraisal_filter_meta_postcode ON " . $wpdb->posts . ".ID = ph_appraisal_filter_meta_postcode.post_id AND ph_appraisal_filter_meta_postcode.meta_key = '_address_postcode'
1875 +";
1876 + }
1877 + elseif ( 'viewing' === $typenow || 'offer' === $typenow || 'sale' === $typenow || 'tenancy' === $typenow )
1878 + {
1879 + $join .= "
1880 +LEFT JOIN " . $wpdb->postmeta . " AS ph_property_filter_meta ON " . $wpdb->posts . ".ID = ph_property_filter_meta.post_id AND ph_property_filter_meta.meta_key = '_property_id'
1881 +LEFT JOIN " . $wpdb->posts . " AS ph_property_filter_posts ON ph_property_filter_posts.ID = ph_property_filter_meta.meta_value
1882 +LEFT JOIN " . $wpdb->postmeta . " AS ph_property_filter_meta_address_concatenated ON ph_property_filter_posts.ID = ph_property_filter_meta_address_concatenated.post_id AND ph_property_filter_meta_address_concatenated.meta_key = '_address_concatenated'
1883 +LEFT JOIN " . $wpdb->postmeta . " AS ph_property_filter_meta_reference_number ON ph_property_filter_posts.ID = ph_property_filter_meta_reference_number.post_id AND ph_property_filter_meta_reference_number.meta_key = '_reference_number'
1884 +LEFT JOIN " . $wpdb->postmeta . " AS ph_applicant_filter_meta ON " . $wpdb->posts . ".ID = ph_applicant_filter_meta.post_id AND ph_applicant_filter_meta.meta_key = '_applicant_contact_id'
1885 +LEFT JOIN " . $wpdb->posts . " AS ph_applicant_filter_posts ON ph_applicant_filter_posts.ID = ph_applicant_filter_meta.meta_value
1886 +";
1887 + }
1888 +
1889 + return $join;
1890 + }
1891 +
1892 + public function posts_where( $where, $q ) {
1893 + global $typenow, $wp_query, $wpdb;
1894 +
1895 + if ( !$q->is_main_query() )
1896 + return $where;
1897 +
1898 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only admin list display or query; no state change.
1899 + $search = isset( $_GET['s'] ) && is_string( $_GET['s'] ) ? sanitize_text_field( wp_unslash( $_GET['s'] ) ) : '';
1900 + if ( $search === '' ) {
1901 + return $where;
1902 + }
1903 + $reference_like = $wpdb->prepare( '%s', $wpdb->esc_like( $search ) . '%' );
1904 + $reference_exact = $wpdb->prepare( '%s', $search );
1905 + $phone_number = '';
1906 +
1907 + if ( 'property' === $typenow )
1908 + {
1909 + $where = preg_replace_callback(
1910 + "/\(\s*" . $wpdb->posts . ".post_title\s+LIKE\s*('(?:\\\\.|[^'\\\\])*')\s*\)/",
1911 + static function( $matches ) use ( $wpdb, $reference_like, $reference_exact, $phone_number ) {
1912 + return "(
1913 + (" . $wpdb->posts . ".post_title LIKE " . $matches[1] . ")
1914 + OR
1915 + (ph_property_filter_meta_address_concatenated.meta_value LIKE " . $matches[1] . ")
1916 + OR
1917 + (ph_property_filter_meta_reference_number.meta_value LIKE " . $reference_like . ")
1918 + OR
1919 + (ph_property_filter_meta_owner_details.meta_value LIKE " . $matches[1] . ")
1920 + )";
1921 + },
1922 + $where
1923 + );
1924 +
1925 + $where = preg_replace(
1926 + "/\s+OR\s+\(\s*" . $wpdb->posts . ".post_excerpt\s+LIKE\s*('(?:\\\\.|[^'\\\\])*')\s*\)/",
1927 + "",
1928 + $where
1929 + );
1930 +
1931 + $where = preg_replace(
1932 + "/\s+OR\s+\(\s*" . $wpdb->posts . ".post_content\s+LIKE\s*('(?:\\\\.|[^'\\\\])*')\s*\)/",
1933 + "",
1934 + $where
1935 + );
1936 + }
1937 + elseif ( 'contact' === $typenow )
1938 + {
1939 + $phone_number = '';
1940 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only admin list display or query; no state change.
1941 + if ( is_numeric(substr($search, 0, 1)) )
1942 + {
1943 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only admin list display or query; no state change.
1944 + $phone_number = preg_replace( "/[^0-9,]/", "", $search );
1945 + }
1946 +
1947 + $where = preg_replace_callback(
1948 + "/\(\s*" . $wpdb->posts . ".post_title\s+LIKE\s*('(?:\\\\.|[^'\\\\])*')\s*\)/",
1949 + static function( $matches ) use ( $wpdb, $reference_like, $reference_exact, $phone_number ) {
1950 + return "(
1951 + (" . $wpdb->posts . ".post_title LIKE " . $matches[1] . ")
1952 + OR
1953 + (ph_contact_filter_meta_address_concatenated.meta_value LIKE " . $matches[1] . ")
1954 + OR
1955 + (ph_contact_filter_meta_email_address.meta_value LIKE " . $matches[1] . ")
1956 + " . ( $phone_number != '' ? "OR (ph_contact_filter_meta_telephone_number.meta_value LIKE '%" . $phone_number . "%')" : '' ) . "
1957 + )";
1958 + },
1959 + $where
1960 + );
1961 +
1962 + $where = preg_replace(
1963 + "/\s+OR\s+\(\s*" . $wpdb->posts . ".post_excerpt\s+LIKE\s*('(?:\\\\.|[^'\\\\])*')\s*\)/",
1964 + "",
1965 + $where
1966 + );
1967 +
1968 + $where = preg_replace(
1969 + "/\s+OR\s+\(\s*" . $wpdb->posts . ".post_content\s+LIKE\s*('(?:\\\\.|[^'\\\\])*')\s*\)/",
1970 + "",
1971 + $where
1972 + );
1973 + }
1974 + elseif ( 'appraisal' === $typenow )
1975 + {
1976 + $where = preg_replace_callback(
1977 + "/\(\s*" . $wpdb->posts . ".post_title\s+LIKE\s*('(?:\\\\.|[^'\\\\])*')\s*\)/",
1978 + static function( $matches ) use ( $wpdb, $reference_like, $reference_exact, $phone_number ) {
1979 + return "(
1980 + (" . $wpdb->posts . ".post_title LIKE " . $matches[1] . ")
1981 + OR
1982 + (ph_appraisal_filter_meta_name_number.meta_value LIKE " . $matches[1] . ")
1983 + OR
1984 + (ph_appraisal_filter_meta_street.meta_value LIKE " . $matches[1] . ")
1985 + OR
1986 + (ph_appraisal_filter_meta_2.meta_value LIKE " . $matches[1] . ")
1987 + OR
1988 + (ph_appraisal_filter_meta_3.meta_value LIKE " . $matches[1] . ")
1989 + OR
1990 + (ph_appraisal_filter_meta_4.meta_value LIKE " . $matches[1] . ")
1991 + OR
1992 + (ph_appraisal_filter_meta_postcode.meta_value LIKE " . $matches[1] . ")
1993 + )";
1994 + },
1995 + $where
1996 + );
1997 + }
1998 + elseif ( 'viewing' === $typenow || 'offer' === $typenow || 'sale' === $typenow || 'tenancy' === $typenow )
1999 + {
2000 + $where = preg_replace_callback(
2001 + "/\(\s*" . $wpdb->posts . ".post_title\s+LIKE\s*('(?:\\\\.|[^'\\\\])*')\s*\)/",
2002 + static function( $matches ) use ( $wpdb, $reference_like, $reference_exact, $phone_number ) {
2003 + return "(
2004 + (" . $wpdb->posts . ".post_title LIKE " . $matches[1] . ")
2005 + OR
2006 + (ph_property_filter_posts.post_title LIKE " . $matches[1] . ")
2007 + OR
2008 + (ph_property_filter_meta_address_concatenated.meta_value LIKE " . $matches[1] . ")
2009 + OR
2010 + (ph_property_filter_meta_reference_number.meta_value = " . $reference_exact . ")
2011 + OR
2012 + (ph_applicant_filter_posts.post_title LIKE " . $matches[1] . ")
2013 + )";
2014 + },
2015 + $where
2016 + );
2017 + }
2018 +
2019 + return $where;
2020 + }
2021 +
1011 2022 /**
1012 2023 * Removes variations etc belonging to a deleted post, and clears transients
1013 2024 *
1014 2025 * @access public
@@ -1072,5 +2083,5 @@
1072 2083 }
1073 2084
1074 2085 endif;
1075 2086
1076 -return new PH_Admin_Post_Types();
2087 +return new PH_Admin_Post_Types();