PluginProbe
Property Hive / 2.3.0
Property Hive v2.3.0
2.3.1 2.3.0 2.2.6 2.2.5 2.2.4 2.2.3 2.2.2 1.4.46 1.4.47 1.4.48 1.4.49 1.4.5 1.4.50 1.4.51 1.4.52 1.4.53 1.4.54 1.4.55 1.4.56 1.4.57 1.4.58 1.4.59 1.4.6 1.4.60 1.4.61 All 261 releases
← All changes | includes/admin/class-ph-admin-post-types.php +1080 -348 1.4.622.3.0 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,19 +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' ) );
32 - add_filter( 'posts_join', array( $this, 'posts_join' ) );
33 - add_filter( 'posts_where', array( $this, 'posts_where' ) );
37 + add_filter( 'posts_join', array( $this, 'posts_join' ), 10, 2 );
38 + add_filter( 'posts_where', array( $this, 'posts_where' ), 10, 2 );
34 39
35 40 // Status transitions
36 41 add_action( 'delete_post', array( $this, 'delete_post' ) );
37 42 add_action( 'wp_trash_post', array( $this, 'trash_post' ) );
38 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;
39 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;
40 267
41 - }
268 + $post_id = isset($_GET['post']) ? intval($_GET['post']) : 0;
269 + $post_type = get_post_type($post_id);
42 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 +
43 306 /**
44 307 * Conditonally load classes and functions only needed when viewing a post type.
45 308 */
46 309 public function include_post_type_handlers() {
@@ -54,8 +317,10 @@
54 317 include( 'post-types/class-ph-admin-cpt-appraisal.php' );
55 318 include( 'post-types/class-ph-admin-cpt-viewing.php' );
56 319 include( 'post-types/class-ph-admin-cpt-offer.php' );
57 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' );
58 323 }
59 324
60 325 /**
61 326 * Change messages when a post type is updated.
@@ -67,19 +332,24 @@
67 332 global $post, $post_ID;
68 333
69 334 $messages['property'] = array(
70 335 0 => '', // Unused. Messages start at index 1.
71 - 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) ) ),
72 338 2 => __( 'Custom field updated.', 'propertyhive' ),
73 339 3 => __( 'Custom field deleted.', 'propertyhive' ),
74 340 4 => __( 'Property updated.', 'propertyhive' ),
75 - 5 => isset($_GET['revision']) ? sprintf( __( 'Property restored to revision from %s', 'propertyhive' ), wp_post_revision_title( (int) $_GET['revision'], false ) ) : false,
76 - 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) ) ),
77 344 7 => __( 'Property saved.', 'propertyhive' ),
78 - 8 => sprintf( __( 'Property submitted. <a target="_blank" href="%s">Preview Property</a>', 'propertyhive' ), esc_url( add_query_arg( 'preview', 'true', get_permalink($post_ID) ) ) ),
79 - 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' ),
80 349 date_i18n( __( 'M j, Y @ G:i', 'propertyhive' ), strtotime( $post->post_date ) ), esc_url( get_permalink($post_ID) ) ),
81 - 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) ) ) ),
82 352 );
83 353
84 354 $messages['contact'] = array(
85 355 0 => '', // Unused. Messages start at index 1.
@@ -86,12 +356,13 @@
86 356 1 => __( 'Contact updated.', 'propertyhive' ),
87 357 2 => __( 'Custom field updated.', 'propertyhive' ),
88 358 3 => __( 'Custom field deleted.', 'propertyhive' ),
89 359 4 => __( 'Contact updated.', 'propertyhive' ),
90 - 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' ),
91 361 6 => __( 'Contact published.', 'propertyhive' ),
92 362 7 => __( 'Contact saved.', 'propertyhive' ),
93 363 8 => __( 'Contact submitted.', 'propertyhive' ),
364 + /* translators: 1: formatted date */
94 365 9 => sprintf( __( 'Contact scheduled for: <strong>%1$s</strong>.', 'propertyhive' ), date_i18n( __( 'M j, Y @ G:i', 'propertyhive' ), strtotime( $post->post_date ) )),
95 366 10 => __( 'Contact draft updated.', 'propertyhive' ),
96 367 );
97 368
@@ -100,12 +371,13 @@
100 371 1 => __( 'Office updated.', 'propertyhive' ),
101 372 2 => __( 'Custom field updated.', 'propertyhive' ),
102 373 3 => __( 'Custom field deleted.', 'propertyhive' ),
103 374 4 => __( 'Office updated.', 'propertyhive' ),
104 - 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' ),
105 376 6 => sprintf( __( 'Office published.', 'propertyhive' ), esc_url( get_permalink($post_ID) ) ),
106 377 7 => __( 'Office saved.', 'propertyhive' ),
107 378 8 => sprintf( __( 'Office submitted.', 'propertyhive' ), esc_url( add_query_arg( 'preview', 'true', get_permalink($post_ID) ) ) ),
379 + /* translators: 1: formatted date */
108 380 9 => sprintf( __( 'Office scheduled for: <strong>%1$s</strong>.', 'propertyhive' ),
109 381 date_i18n( __( 'M j, Y @ G:i', 'propertyhive' ), strtotime( $post->post_date ) ), esc_url( get_permalink($post_ID) ) ),
110 382 10 => sprintf( __( 'Office draft updated. ', 'propertyhive' ), esc_url( add_query_arg( 'preview', 'true', get_permalink($post_ID) ) ) ),
111 383 );
@@ -115,12 +387,13 @@
115 387 1 => sprintf( __( 'Enquiry updated.', 'propertyhive' ), esc_url( get_permalink($post_ID) ) ),
116 388 2 => __( 'Custom field updated.', 'propertyhive' ),
117 389 3 => __( 'Custom field deleted.', 'propertyhive' ),
118 390 4 => __( 'Enquiry updated.', 'propertyhive' ),
119 - 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' ),
120 392 6 => sprintf( __( 'Enquiry published.', 'propertyhive' ), esc_url( get_permalink($post_ID) ) ),
121 393 7 => __( 'Enquiry saved.', 'propertyhive' ),
122 394 8 => sprintf( __( 'Enquiry submitted.', 'propertyhive' ), esc_url( add_query_arg( 'preview', 'true', get_permalink($post_ID) ) ) ),
395 + /* translators: 1: formatted date */
123 396 9 => sprintf( __( 'Enquiry scheduled for: <strong>%1$s</strong>.', 'propertyhive' ),
124 397 date_i18n( __( 'M j, Y @ G:i', 'propertyhive' ), strtotime( $post->post_date ) ), esc_url( get_permalink($post_ID) ) ),
125 398 10 => sprintf( __( 'Enquiry draft updated.', 'propertyhive' ), esc_url( add_query_arg( 'preview', 'true', get_permalink($post_ID) ) ) ),
126 399 );
@@ -132,10 +405,13 @@
132 405 * Remove month filter from some property hive pages
133 406 */
134 407 public function remove_month_filter() {
135 408 global $typenow;
136 -
137 - 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) )
138 414 {
139 415 add_filter('months_dropdown_results', '__return_empty_array');
140 416 }
141 417 }
@@ -181,8 +457,14 @@
181 457 break;
182 458 case 'sale' :
183 459 $this->sale_filters();
184 460 break;
461 + case 'tenancy' :
462 + $this->tenancy_filters();
463 + break;
464 + case 'key_date' :
465 + $this->key_date_filters();
466 + break;
185 467 default :
186 468 break;
187 469 }
188 470 }
@@ -200,10 +482,11 @@
200 482 $output .= $this->property_marketing_filter();
201 483 $output .= $this->property_availability_filter();
202 484 $output .= $this->property_location_filter();
203 485 $output .= $this->property_office_filter();
204 - $output .= $this->property_negotiator_filter();
486 + $output .= $this->negotiator_filter();
205 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.
206 489 echo apply_filters( 'propertyhive_property_filters', $output );
207 490 }
208 491
209 492 /**
@@ -213,22 +496,24 @@
213 496 global $wp_query;
214 497
215 498 $departments = ph_get_departments();
216 499
217 - $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 : '';
218 503
219 504 // Department filtering
220 505 $output = '<select name="_department" id="dropdown_property_department">';
221 506
222 - $output .= '<option value="">' . __( 'All Departments', 'propertyhive' ) . '</option>';
507 + $output .= '<option value="">' . esc_html__( 'All Departments', 'propertyhive' ) . '</option>';
223 508
224 509 foreach ( $departments as $key => $value )
225 510 {
226 511 if ( get_option( 'propertyhive_active_departments_' . str_replace("residential-", "", $key) ) == 'yes' )
227 512 {
228 - $output .= '<option value="' . $key . '"';
513 + $output .= '<option value="' . esc_attr($key) . '"';
229 514 $output .= selected( $key, $selected_department, false );
230 - $output .= '>' . $value . '</option>';
515 + $output .= '>' . esc_html($value) . '</option>';
231 516 }
232 517 }
233 518
234 519 $output .= '</select>';
@@ -244,9 +529,9 @@
244 529
245 530 // Department filtering
246 531 $output = '<select name="_office_id" id="dropdown_property_office_id">';
247 532
248 - $output .= '<option value="">' . __( 'All Offices', 'propertyhive' ) . '</option>';
533 + $output .= '<option value="">' . esc_html__( 'All Offices', 'propertyhive' ) . '</option>';
249 534
250 535 $args = array(
251 536 'post_type' => 'office',
252 537 'nopaging' => true,
@@ -260,14 +545,16 @@
260 545 while ($office_query->have_posts())
261 546 {
262 547 $office_query->the_post();
263 548
264 - $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.
265 551 if ( isset( $_GET['_office_id'] ) && ! empty( $_GET['_office_id'] ) )
266 552 {
553 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only admin list display or query; no state change.
267 554 $output .= selected( $post->ID, (int)$_GET['_office_id'], false );
268 555 }
269 - $output .= '>' . get_the_title() . '</option>';
556 + $output .= '>' . esc_html(get_the_title()) . '</option>';
270 557 }
271 558 }
272 559
273 560 wp_reset_postdata();
@@ -277,32 +564,51 @@
277 564 return $output;
278 565 }
279 566
280 567 /**
281 - * Show a property negotiator filter box
568 + * Show a negotiator filter box
282 569 */
283 - public function property_negotiator_filter() {
284 - global $wp_query, $post;
285 -
286 - $selected = '';
287 - if ( isset( $_GET['_negotiator_id'] ) && ! empty( $_GET['_negotiator_id'] ) )
288 - {
289 - $selected = (int)$_GET['_negotiator_id'];
290 - }
291 -
292 - $args = array(
570 + public function negotiator_filter() {
571 +
572 + return wp_dropdown_users(array(
293 573 'name' => '_negotiator_id',
294 574 'id' => 'dropdown_property_negotiator_id',
295 - 'show_option_all' => __( 'All Negotiators', 'propertyhive' ),
296 - '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'],
297 578 'echo' => false,
298 - 'role__not_in' => array('property_hive_contact')
299 - );
300 - $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 + }
301 583
302 - return $output;
303 - }
584 + /**
585 + * Show a date range selector
586 + */
587 + public function date_range_filter() {
304 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 +
305 611 /**
306 612 * Show a property location filter box
307 613 */
308 614 public function property_location_filter() {
@@ -315,9 +621,9 @@
315 621 $args = array(
316 622 'hide_empty' => false,
317 623 'parent' => 0
318 624 );
319 - $terms = get_terms( 'location', $args );
625 + $terms = get_terms( array_merge( wp_parse_args( $args ), array( 'taxonomy' => 'location' ) ) );
320 626
321 627 if ( !empty( $terms ) && !is_wp_error( $terms ) )
322 628 {
323 629 foreach ($terms as $term)
@@ -327,9 +633,9 @@
327 633 $args = array(
328 634 'hide_empty' => false,
329 635 'parent' => $term->term_id
330 636 );
331 - $subterms = get_terms( 'location', $args );
637 + $subterms = get_terms( array_merge( wp_parse_args( $args ), array( 'taxonomy' => 'location' ) ) );
332 638
333 639 if ( !empty( $subterms ) && !is_wp_error( $subterms ) )
334 640 {
335 641 foreach ($subterms as $term)
@@ -339,9 +645,9 @@
339 645 $args = array(
340 646 'hide_empty' => false,
341 647 'parent' => $term->term_id
342 648 );
343 - $subsubterms = get_terms( 'location', $args );
649 + $subsubterms = get_terms( array_merge( wp_parse_args( $args ), array( 'taxonomy' => 'location' ) ) );
344 650
345 651 if ( !empty( $subsubterms ) && !is_wp_error( $subsubterms ) )
346 652 {
347 653 foreach ($subsubterms as $term)
@@ -353,20 +659,22 @@
353 659 }
354 660 }
355 661 }
356 662
357 - $output .= '<option value="">' . __( 'All Locations', 'propertyhive' ) . '</option>';
663 + $output .= '<option value="">' . esc_html(__( 'All Locations', 'propertyhive' )) . '</option>';
358 664
359 665 if ( !empty($options) )
360 666 {
361 667 foreach ( $options as $value => $label )
362 668 {
363 - $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.
364 671 if ( isset( $_GET['_location_id'] ) && ! empty( $_GET['_location_id'] ) )
365 672 {
673 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only admin list display or query; no state change.
366 674 $output .= selected( $value, (int)$_GET['_location_id'], false );
367 675 }
368 - $output .= '>' . $label . '</option>';
676 + $output .= '>' . esc_html($label) . '</option>';
369 677 }
370 678 }
371 679
372 680 $output .= '</select>';
@@ -387,9 +695,9 @@
387 695 $args = array(
388 696 'hide_empty' => false,
389 697 'parent' => 0
390 698 );
391 - $terms = get_terms( 'availability', $args );
699 + $terms = get_terms( array_merge( wp_parse_args( $args ), array( 'taxonomy' => 'availability' ) ) );
392 700
393 701 if ( !empty( $terms ) && !is_wp_error( $terms ) )
394 702 {
395 703 foreach ($terms as $term)
@@ -397,20 +705,22 @@
397 705 $options[$term->term_id] = $term->name;
398 706 }
399 707 }
400 708
401 - $output .= '<option value="">' . __( 'All Availabilities', 'propertyhive' ) . '</option>';
709 + $output .= '<option value="">' . esc_html(__( 'All Availabilities', 'propertyhive' )) . '</option>';
402 710
403 711 if ( !empty($options) )
404 712 {
405 713 foreach ( $options as $value => $label )
406 714 {
407 - $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.
408 717 if ( isset( $_GET['_availability_id'] ) && ! empty( $_GET['_availability_id'] ) )
409 718 {
719 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only admin list display or query; no state change.
410 720 $output .= selected( $value, (int)$_GET['_availability_id'], false );
411 721 }
412 - $output .= '>' . $label . '</option>';
722 + $output .= '>' . esc_html($label) . '</option>';
413 723 }
414 724 }
415 725
416 726 $output .= '</select>';
@@ -426,9 +736,9 @@
426 736
427 737 // Availability filtering
428 738 $output = '<select name="_marketing" id="dropdown_property_marketing">';
429 739
430 - $output .= '<option value="">' . __( 'All Marketing Statuses', 'propertyhive' ) . '</option>';
740 + $output .= '<option value="">' . esc_html__( 'All Marketing Statuses', 'propertyhive' ) . '</option>';
431 741
432 742 $options = array(
433 743 'on_market' => __( 'On Market Only', 'propertyhive' ),
434 744 'off_market' => __( 'Not On Market Only', 'propertyhive' ),
@@ -438,9 +748,9 @@
438 748 $args = array(
439 749 'hide_empty' => false,
440 750 'parent' => 0
441 751 );
442 - $terms = get_terms( 'marketing_flag', $args );
752 + $terms = get_terms( array_merge( wp_parse_args( $args ), array( 'taxonomy' => 'marketing_flag' ) ) );
443 753
444 754 if ( !empty( $terms ) && !is_wp_error( $terms ) )
445 755 {
446 756 foreach ($terms as $term)
@@ -449,17 +759,18 @@
449 759 }
450 760 }
451 761
452 762 $options = apply_filters( 'propertyhive_property_filter_marketing_options', $options );
763 + $selected_marketing = $this->get_admin_query_value( '_marketing' );
453 764
454 765 foreach ( $options as $key => $value )
455 766 {
456 - $output .= '<option value="' . $key . '"';
457 - if ( isset( $_GET['_marketing'] ) && ! empty( $_GET['_marketing'] ) )
767 + $output .= '<option value="' . esc_attr($key) . '"';
768 + if ( ! empty( $selected_marketing ) )
458 769 {
459 - $output .= selected( $key, sanitize_text_field($_GET['_marketing']), false );
770 + $output .= selected( $key, $selected_marketing, false );
460 771 }
461 - $output .= '>' . $value . '</option>';
772 + $output .= '>' . esc_html($value) . '</option>';
462 773 }
463 774
464 775 $output .= '</select>';
465 776
@@ -471,9 +782,11 @@
471 782 */
472 783 public function contact_filters() {
473 784 global $wp_query;
474 785
475 - $selected_contact_type = isset( $_GET['_contact_type'] ) && in_array( $_GET['_contact_type'], array( 'owner', 'potentialowner', 'applicant', 'hotapplicant', '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 : '';
476 789
477 790 // Type filtering
478 791 $options = array();
479 792
@@ -479,9 +792,9 @@
479 792
480 793 // Owners
481 794 $option = '<option value="owner"';
482 795 $option .= selected( 'owner', $selected_contact_type, false );
483 - $option .= '>' . __( 'Owners and Landlords', 'propertyhive' ) . '</option>';
796 + $option .= '>' . esc_html(__( 'Owners and Landlords', 'propertyhive' )) . '</option>';
484 797
485 798 $options[] = $option;
486 799
487 800 // Potential Owners
@@ -486,9 +799,9 @@
486 799
487 800 // Potential Owners
488 801 $option = '<option value="potentialowner"';
489 802 $option .= selected( 'potentialowner', $selected_contact_type, false );
490 - $option .= '>' . __( 'Potential Owners and Landlords', 'propertyhive' ) . '</option>';
803 + $option .= '>' . esc_html(__( 'Potential Owners and Landlords', 'propertyhive' )) . '</option>';
491 804
492 805 $options[] = $option;
493 806
494 807 // Applicants
@@ -493,9 +806,9 @@
493 806
494 807 // Applicants
495 808 $option = '<option value="applicant"';
496 809 $option .= selected( 'applicant', $selected_contact_type, false );
497 - $option .= '>' . __( 'Applicants', 'propertyhive' ) . '</option>';
810 + $option .= '>' . esc_html(__( 'Applicants', 'propertyhive' )) . '</option>';
498 811
499 812 $options[] = $option;
500 813
501 814 // Hot Applicants
@@ -500,9 +813,9 @@
500 813
501 814 // Hot Applicants
502 815 $option = '<option value="hotapplicant"';
503 816 $option .= selected( 'hotapplicant', $selected_contact_type, false );
504 - $option .= '>- ' . __( 'Hot Applicants', 'propertyhive' ) . '</option>';
817 + $option .= '>- ' . esc_html(__( 'Hot Applicants', 'propertyhive' )) . '</option>';
505 818
506 819 $options[] = $option;
507 820
508 821 // Third Parties
@@ -507,9 +820,9 @@
507 820
508 821 // Third Parties
509 822 $option = '<option value="thirdparty"';
510 823 $option .= selected( 'thirdparty', $selected_contact_type, false );
511 - $option .= '>' . __( 'Third Party Contacts', 'propertyhive' ) . '</option>';
824 + $option .= '>' . esc_html(__( 'Third Party Contacts', 'propertyhive' )) . '</option>';
512 825
513 826 $options[] = $option;
514 827
515 828 $options = apply_filters( 'propertyhive_contact_filter_options', $options );
@@ -518,9 +831,9 @@
518 831 if (count($options) > 1)
519 832 {
520 833 $output = '<select name="_contact_type" id="dropdown_contact_type">';
521 834
522 - $output .= '<option value="">' . __( 'Show all contact types', 'propertyhive' ) . '</option>';
835 + $output .= '<option value="">' . esc_html(__( 'Show all contact types', 'propertyhive' )) . '</option>';
523 836
524 837 $output .= implode("", $options);
525 838
526 839 $output .= '</select>';
@@ -525,9 +838,12 @@
525 838
526 839 $output .= '</select>';
527 840 }
528 841
529 - 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 );
530 846 }
531 847
532 848 /**
533 849 * Show an enquiry filter box
@@ -537,12 +853,15 @@
537 853
538 854 // Department filtering
539 855 $output = '';
540 856
857 + $output .= $this->date_range_filter();
541 858 $output .= $this->enquiry_status_filter();
542 859 $output .= $this->enquiry_source_filter();
543 860 $output .= $this->enquiry_office_filter();
861 + $output .= $this->enquiry_negotiator_filter();
544 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.
545 864 echo apply_filters( 'propertyhive_enquiry_filters', $output );
546 865 }
547 866
548 867 /**
@@ -550,21 +869,30 @@
550 869 */
551 870 public function enquiry_status_filter() {
552 871 global $wp_query;
553 872
554 - $selected_status = isset( $_GET['_status'] ) && in_array( $_GET['_status'], array( 'open', 'closed' ) ) ? $_GET['_status'] : '';
555 -
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 +
556 877 // Status filtering
557 - $output = '<select name="_status" id="dropdown_enquiry_status">';
558 -
559 - $output .= '<option value="open"';
560 - $output .= selected( 'open', $selected_status, false );
561 - $output .= '>' . __( 'Open', 'propertyhive' ) . '</option>';
878 + $output = '<select name="_status" id="dropdown_enquiry_status">
879 + <option value="all"' . selected( 'all', $selected_status, false ) . '>All</option>';
562 880
563 - $output .= '<option value="closed"';
564 - $output .= selected( 'closed', $selected_status, false );
565 - $output .= '>' . __( 'Closed', 'propertyhive' ) . '</option>';
566 -
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 +
567 895 $output .= '</select>';
568 896
569 897 return $output;
570 898 }
@@ -585,19 +913,20 @@
585 913 asort($sources);
586 914
587 915 // Status filtering
588 916 $output = '<select name="_source" id="dropdown_enquiry_source">';
917 + $selected_source = $this->get_admin_query_value( '_source' );
589 918
590 - $output .= '<option value="">' . __( 'Show all sources', 'propertyhive' ) . '</option>';
919 + $output .= '<option value="">' . esc_html__( 'Show all sources', 'propertyhive' ) . '</option>';
591 920
592 921 foreach ( $sources as $key => $value )
593 922 {
594 - $output .= '<option value="' . $key . '"';
595 - if ( isset( $_GET['_source'] ) && ! empty( $_GET['_source'] ) )
923 + $output .= '<option value="' . esc_attr($key) . '"';
924 + if ( ! empty( $selected_source ) )
596 925 {
597 - $output .= selected( $key, sanitize_text_field($_GET['_source']), false );
926 + $output .= selected( $key, $selected_source, false );
598 927 }
599 - $output .= '>' . __( $value, 'propertyhive' ) . '</option>';
928 + $output .= '>' . esc_html( $value ) . '</option>';
600 929 }
601 930
602 931 $output .= '</select>';
603 932
@@ -612,9 +941,9 @@
612 941
613 942 // Department filtering
614 943 $output = '<select name="_office_id" id="dropdown_enquiry_office_id">';
615 944
616 - $output .= '<option value="">' . __( 'All Offices', 'propertyhive' ) . '</option>';
945 + $output .= '<option value="">' . esc_html__( 'All Offices', 'propertyhive' ) . '</option>';
617 946
618 947 $args = array(
619 948 'post_type' => 'office',
620 949 'nopaging' => true,
@@ -628,14 +957,16 @@
628 957 while ($office_query->have_posts())
629 958 {
630 959 $office_query->the_post();
631 960
632 - $output .= '<option value="' . $post->ID . '"';
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.
633 963 if ( isset( $_GET['_office_id'] ) && ! empty( $_GET['_office_id'] ) )
634 964 {
965 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only admin list display or query; no state change.
635 966 $output .= selected( $post->ID, (int)$_GET['_office_id'], false );
636 967 }
637 - $output .= '>' . get_the_title() . '</option>';
968 + $output .= '>' . esc_html(get_the_title()) . '</option>';
638 969 }
639 970 }
640 971
641 972 wp_reset_postdata();
@@ -645,8 +976,24 @@
645 976 return $output;
646 977 }
647 978
648 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 + /**
649 996 * Show am appraisal filter box
650 997 */
651 998 public function appraisal_filters() {
652 999 global $wp_query;
@@ -653,10 +1000,12 @@
653 1000
654 1001 $output = '';
655 1002
656 1003 $output .= $this->appraisal_status_filter();
657 - $output .= $this->appraisal_attending_negotiator_filter();
1004 + $output .= $this->negotiator_filter();
1005 + $output .= $this->date_range_filter();
658 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.
659 1008 echo apply_filters( 'propertyhive_appraisal_filters', $output );
660 1009 }
661 1010
662 1011 /**
@@ -664,38 +1013,40 @@
664 1013 */
665 1014 public function appraisal_status_filter() {
666 1015 global $wp_query;
667 1016
668 - $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 : '';
669 1020
670 1021 // Status filtering
671 1022 $output = '<select name="_status" id="dropdown_appraisal_status">';
672 1023
673 - $output .= '<option value="">All Statuses</option>';
1024 + $output .= '<option value="">' . esc_html__( 'All Statuses', 'propertyhive' ) . '</option>';
674 1025
675 1026 $output .= '<option value="pending"';
676 1027 $output .= selected( 'pending', $selected_status, false );
677 - $output .= '>' . __( 'Pending', 'propertyhive' ) . '</option>';
1028 + $output .= '>' . esc_html(__( 'Pending', 'propertyhive' )) . '</option>';
678 1029
679 1030 $output .= '<option value="carried_out"';
680 1031 $output .= selected( 'carried_out', $selected_status, false );
681 - $output .= '>' . __( 'Carried Out', 'propertyhive' ) . '</option>';
1032 + $output .= '>' . esc_html(__( 'Carried Out', 'propertyhive' )) . '</option>';
682 1033
683 1034 $output .= '<option value="won"';
684 1035 $output .= selected( 'won', $selected_status, false );
685 - $output .= '>- ' . __( 'Won', 'propertyhive' ) . '</option>';
1036 + $output .= '>- ' . esc_html(__( 'Won', 'propertyhive' )) . '</option>';
686 1037
687 1038 $output .= '<option value="lost"';
688 1039 $output .= selected( 'lost', $selected_status, false );
689 - $output .= '>- ' . __( 'Lost', 'propertyhive' ) . '</option>';
1040 + $output .= '>- ' . esc_html(__( 'Lost', 'propertyhive' )) . '</option>';
690 1041
691 1042 $output .= '<option value="instructed"';
692 1043 $output .= selected( 'instructed', $selected_status, false );
693 - $output .= '>- ' . __( 'Instructed', 'propertyhive' ) . '</option>';
1044 + $output .= '>- ' . esc_html(__( 'Instructed', 'propertyhive' )) . '</option>';
694 1045
695 1046 $output .= '<option value="cancelled"';
696 1047 $output .= selected( 'cancelled', $selected_status, false );
697 - $output .= '>' . __( 'Cancelled', 'propertyhive' ) . '</option>';
1048 + $output .= '>' . esc_html(__( 'Cancelled', 'propertyhive' )) . '</option>';
698 1049
699 1050 $output .= '</select>';
700 1051
701 1052 return $output;
@@ -701,58 +1052,22 @@
701 1052 return $output;
702 1053 }
703 1054
704 1055 /**
705 - * Show an appraisal attending negotiator filter box
706 - */
707 - public function appraisal_attending_negotiator_filter() {
708 - global $wp_query;
709 -
710 - $selected_negotiator_id = isset( $_GET['_negotiator_id']) ? (int)$_GET['_negotiator_id'] : '';
711 -
712 - // Status filtering
713 - $output = '<select name="_negotiator_id" id="dropdown_appraisal_negotiator_id">';
714 -
715 - $output .= '<option value="">Attending Negotiator</option>';
716 - $output .= '<option value="">All Negotiators</option>';
717 -
718 - $args = array(
719 - 'number' => 9999,
720 - 'orderby' => 'display_name',
721 - 'role__not_in' => array('property_hive_contact')
722 - );
723 - $user_query = new WP_User_Query( $args );
724 -
725 - if ( ! empty( $user_query->results ) )
726 - {
727 - foreach ( $user_query->results as $user )
728 - {
729 - $output .= '<option value="' . $user->ID . '"';
730 - if ( $user->ID == $selected_negotiator_id )
731 - {
732 - $output .= ' selected';
733 - }
734 - $output .= '>' . $user->display_name . '</option>';
735 - }
736 - }
737 -
738 - $output .= '</select>';
739 -
740 - return $output;
741 - }
742 -
743 - /**
744 1056 * Show a viewing filter box
745 1057 */
746 1058 public function viewing_filters() {
747 1059 global $wp_query;
748 -
1060 +
749 1061 // Department filtering
750 1062 $output = '';
751 -
1063 +
752 1064 $output .= $this->viewing_status_filter();
753 - $output .= $this->viewing_attending_negotiator_filter();
1065 + $output .= $this->property_office_filter();
1066 + $output .= $this->negotiator_filter();
1067 + $output .= $this->date_range_filter();
754 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.
755 1070 echo apply_filters( 'propertyhive_viewing_filters', $output );
756 1071 }
757 1072
758 1073 /**
@@ -760,85 +1075,56 @@
760 1075 */
761 1076 public function viewing_status_filter() {
762 1077 global $wp_query;
763 1078
764 - $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'] : '';
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 : '';
765 1082
766 1083 // Status filtering
767 1084 $output = '<select name="_status" id="dropdown_viewing_status">';
768 -
769 - $output .= '<option value="">All Statuses</option>';
770 1085
771 - $output .= '<option value="pending"';
772 - $output .= selected( 'pending', $selected_status, false );
773 - $output .= '>' . __( 'Pending', 'propertyhive' ) . '</option>';
1086 + $output .= '<option value="">' . esc_html__( 'All Statuses', 'propertyhive' ) . '</option>';
774 1087
775 - $output .= '<option value="confirmed"';
776 - $output .= selected( 'confirmed', $selected_status, false );
777 - $output .= '>- ' . __( 'Confirmed', 'propertyhive' ) . '</option>';
1088 + $viewing_statuses = ph_get_viewing_statuses();
778 1089
779 - $output .= '<option value="unconfirmed"';
780 - $output .= selected( 'unconfirmed', $selected_status, false );
781 - $output .= '>- ' . __( 'Awaiting Confirmation', 'propertyhive' ) . '</option>';
1090 + foreach ( $viewing_statuses as $status => $display_status )
1091 + {
1092 + $output .= '<option value="' . esc_attr($status) . '"';
1093 + $output .= selected( $status, $selected_status, false );
1094 + $output .= '>' . esc_html($display_status) . '</option>';
1095 + }
782 1096
783 - $output .= '<option value="carried_out"';
784 - $output .= selected( 'carried_out', $selected_status, false );
785 - $output .= '>' . __( 'Carried Out', 'propertyhive' ) . '</option>';
1097 + $output .= '</select>';
786 1098
787 - $output .= '<option value="feedback_passed_on"';
788 - $output .= selected( 'feedback_passed_on', $selected_status, false );
789 - $output .= '>- ' . __( 'Feedback Passed On', 'propertyhive' ) . '</option>';
1099 + return $output;
1100 + }
790 1101
791 - $output .= '<option value="feedback_not_passed_on"';
792 - $output .= selected( 'feedback_not_passed_on', $selected_status, false );
793 - $output .= '>- ' . __( 'Feedback Not Passed On', 'propertyhive' ) . '</option>';
794 1102
795 - $output .= '<option value="cancelled"';
796 - $output .= selected( 'cancelled', $selected_status, false );
797 - $output .= '>' . __( 'Cancelled', 'propertyhive' ) . '</option>';
798 -
799 - $output .= '</select>';
1103 + public function refresh_property_office_filtering( $query ) {
1104 + remove_filter('posts_join', array( $this, 'filter_by_property_office') );
800 1105
801 - return $output;
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 + };
802 1114 }
803 1115
804 - /**
805 - * Show a viewing attending negotiator filter box
806 - */
807 - public function viewing_attending_negotiator_filter() {
808 - global $wp_query;
809 1116
810 - $selected_negotiator_id = isset( $_GET['_negotiator_id']) ? (int)$_GET['_negotiator_id'] : '';
811 -
812 - // Status filtering
813 - $output = '<select name="_negotiator_id" id="dropdown_viewing_negotiator_id">';
814 -
815 - $output .= '<option value="">Attending Negotiator</option>';
816 - $output .= '<option value="">All Negotiators</option>';
1117 + public function filter_by_property_office($query) {
1118 + global $wpdb;
817 1119
818 - $args = array(
819 - 'number' => 9999,
820 - 'orderby' => 'display_name',
821 - 'role__not_in' => array('property_hive_contact')
822 - );
823 - $user_query = new WP_User_Query( $args );
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;
824 1122
825 - if ( ! empty( $user_query->results ) )
826 - {
827 - foreach ( $user_query->results as $user )
828 - {
829 - $output .= '<option value="' . $user->ID . '"';
830 - if ( $user->ID == $selected_negotiator_id )
831 - {
832 - $output .= ' selected';
833 - }
834 - $output .= '>' . $user->display_name . '</option>';
835 - }
836 - }
837 -
838 - $output .= '</select>';
839 -
840 - return $output;
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;
841 1127 }
842 1128
843 1129 /**
844 1130 * Show an offer filter box
@@ -848,9 +1134,12 @@
848 1134
849 1135 $output = '';
850 1136
851 1137 $output .= $this->offer_status_filter();
1138 + $output .= $this->property_office_filter();
1139 + $output .= $this->date_range_filter();
852 1140
1141 + // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Built-in controls escape their text and attributes before this trusted PHP filter adds complete HTML controls.
853 1142 echo apply_filters( 'propertyhive_offer_filters', $output );
854 1143 }
855 1144
856 1145 /**
@@ -858,27 +1147,26 @@
858 1147 */
859 1148 public function offer_status_filter() {
860 1149 global $wp_query;
861 1150
862 - $selected_status = isset( $_GET['_status'] ) && in_array( $_GET['_status'], array( 'pending', 'accepted', 'declined' ) ) ? $_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 : '';
863 1154
864 1155 // Status filtering
865 1156 $output = '<select name="_status" id="dropdown_offer_status">';
866 -
867 - $output .= '<option value="">All Statuses</option>';
868 1157
869 - $output .= '<option value="pending"';
870 - $output .= selected( 'pending', $selected_status, false );
871 - $output .= '>' . __( 'Pending', 'propertyhive' ) . '</option>';
1158 + $output .= '<option value="">' . esc_html(__( 'All Statuses', 'propertyhive' )) . '</option>';
872 1159
873 - $output .= '<option value="accepted"';
874 - $output .= selected( 'accepted', $selected_status, false );
875 - $output .= '>' . __( 'Accepted', 'propertyhive' ) . '</option>';
1160 + $offer_statuses = ph_get_offer_statuses();
876 1161
877 - $output .= '<option value="declined"';
878 - $output .= selected( 'declined', $selected_status, false );
879 - $output .= '>' . __( 'Declined', 'propertyhive' ) . '</option>';
880 -
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 +
881 1169 $output .= '</select>';
882 1170
883 1171 return $output;
884 1172 }
@@ -891,9 +1179,12 @@
891 1179
892 1180 $output = '';
893 1181
894 1182 $output .= $this->sale_status_filter();
1183 + $output .= $this->property_office_filter();
1184 + $output .= $this->date_range_filter();
895 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.
896 1187 echo apply_filters( 'propertyhive_sale_filters', $output );
897 1188 }
898 1189
899 1190 /**
@@ -901,35 +1192,193 @@
901 1192 */
902 1193 public function sale_status_filter() {
903 1194 global $wp_query;
904 1195
905 - $selected_status = isset( $_GET['_status'] ) && in_array( $_GET['_status'], array( 'current', 'exchanged', 'completed', 'fallen_through' ) ) ? $_GET['_status'] : '';
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 : '';
906 1199
907 1200 // Status filtering
908 1201 $output = '<select name="_status" id="dropdown_sale_status">';
909 1202
910 - $output .= '<option value="">All Statuses</option>';
1203 + $output .= '<option value="">' . esc_html__( 'All Statuses', 'propertyhive' ) . '</option>';
911 1204
1205 + $sale_statuses = ph_get_sale_statuses();
1206 +
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>';
1215 +
1216 + return $output;
1217 + }
1218 +
1219 + /**
1220 + * Show an tenancy filter box
1221 + */
1222 + public function tenancy_filters() {
1223 + global $wp_query;
1224 +
1225 + $output = '';
1226 +
1227 + $output .= $this->tenancy_status_filter();
1228 + $output .= $this->tenancy_management_type_filter();
1229 +
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 +
912 1253 $output .= '<option value="current"';
913 1254 $output .= selected( 'current', $selected_status, false );
914 - $output .= '>' . __( 'Current', 'propertyhive' ) . '</option>';
1255 + $output .= '> ' . esc_html(__( 'Current', 'propertyhive' )) . '</option>';
915 1256
916 - $output .= '<option value="exchanged"';
917 - $output .= selected( 'exchanged', $selected_status, false );
918 - $output .= '>' . __( 'Exchanged', 'propertyhive' ) . '</option>';
1257 + $output .= '<option value="finished"';
1258 + $output .= selected( 'finished', $selected_status, false );
1259 + $output .= '> ' . esc_html(__( 'Finished', 'propertyhive' )) . '</option>';
919 1260
920 - $output .= '<option value="completed"';
921 - $output .= selected( 'completed', $selected_status, false );
922 - $output .= '>' . __( 'Completed', 'propertyhive' ) . '</option>';
1261 + $output .= '</select>';
923 1262
924 - $output .= '<option value="fallen_through"';
925 - $output .= selected( 'fallen_through', $selected_status, false );
926 - $output .= '>' . __( 'Fallen Through', 'propertyhive' ) . '</option>';
927 -
1263 + return $output;
1264 + }
1265 +
1266 + /**
1267 + * Show an tenancy management type filter box
1268 + */
1269 + public function tenancy_management_type_filter() {
1270 + global $wp_query;
1271 +
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 +
1281 + // Status filtering
1282 + $output = '<select name="_management_type" id="dropdown_tenancy_management_type">';
1283 +
1284 + $output .= '<option value="">' . esc_html(__( 'All Management Types', 'propertyhive' )) . '</option>';
1285 +
1286 + foreach ( $management_types as $key => $value )
1287 + {
1288 + $output .= '<option value="' . esc_attr($key) . '"';
1289 + $output .= selected( $key, $selected_management_type, false );
1290 + $output .= '>' . esc_html( $value ) . '</option>';
1291 + }
1292 +
928 1293 $output .= '</select>';
929 1294
930 1295 return $output;
931 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 + }
932 1381
933 1382 /**
934 1383 * Filters and sorting handler
935 1384 * @param array $vars
@@ -937,50 +1386,71 @@
937 1386 */
938 1387 public function request_query( $vars ) {
939 1388 global $typenow, $wp_query;
940 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.
941 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.
942 1393 if ( !isset($vars['tax_query']) ) { $vars['tax_query'] = array(); }
943 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 +
944 1403 if ( 'property' === $typenow )
945 1404 {
946 - 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 ) ) {
947 1407 $vars['meta_query'][] = array(
948 1408 'key' => '_department',
949 - 'value' => sanitize_text_field( $_GET['_department'] ),
1409 + 'value' => $department,
950 1410 );
951 1411 }
1412 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only admin list display or query; no state change.
952 1413 if ( ! empty( $_GET['_office_id'] ) ) {
953 1414 $vars['meta_query'][] = array(
954 1415 'key' => '_office_id',
1416 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only admin list display or query; no state change.
955 1417 'value' => (int)$_GET['_office_id'],
956 1418 );
957 1419 }
1420 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only admin list display or query; no state change.
958 1421 if ( ! empty( $_GET['_negotiator_id'] ) ) {
959 1422 $vars['meta_query'][] = array(
960 1423 'key' => '_negotiator_id',
1424 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only admin list display or query; no state change.
961 1425 'value' => (int)$_GET['_negotiator_id'],
962 1426 );
963 1427 }
1428 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only admin list display or query; no state change.
964 1429 if ( ! empty( $_GET['_location_id'] ) ) {
965 1430 $vars['tax_query'][] = array(
966 1431 'taxonomy' => 'location',
1432 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only admin list display or query; no state change.
967 1433 'terms' => ( (is_array($_GET['_location_id'])) ? (int)$_GET['_location_id'] : array( (int)$_GET['_location_id'] ) )
968 1434 );
969 1435 }
1436 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only admin list display or query; no state change.
970 1437 if ( ! empty( $_GET['_availability_id'] ) ) {
971 1438 $vars['tax_query'][] = array(
972 1439 'taxonomy' => 'availability',
1440 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only admin list display or query; no state change.
973 1441 'terms' => ( (is_array($_GET['_availability_id'])) ? (int)$_GET['_availability_id'] : array( (int)$_GET['_availability_id'] ) )
974 1442 );
975 1443 }
976 - 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 ) {
977 1446 $vars['meta_query'][] = array(
978 1447 'key' => '_on_market',
979 1448 'value' => 'yes',
980 1449 );
981 1450 }
982 - 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 ) {
983 1453 $vars['meta_query'][] = array(
984 1454 'key' => '_on_market',
985 1455 'value' => 'yes',
986 1456 'compare' => '!=',
@@ -985,16 +1455,18 @@
985 1455 'value' => 'yes',
986 1456 'compare' => '!=',
987 1457 );
988 1458 }
989 - 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 ) {
990 1461 $vars['meta_query'][] = array(
991 1462 'key' => '_featured',
992 1463 'value' => 'yes',
993 1464 );
994 - }
995 - if ( ! empty( $_GET['_marketing'] ) && substr($_GET['_marketing'], 0, 15) == 'marketing_flag_' ) {
996 - $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 );
997 1469 $vars['tax_query'][] = array(
998 1470 'taxonomy' => 'marketing_flag',
999 1471 'terms' => ( (is_array($marketing_flag_id)) ? $marketing_flag_id : array( $marketing_flag_id ) )
1000 1472 );
@@ -1001,11 +1473,11 @@
1001 1473 }
1002 1474 }
1003 1475 elseif ( 'contact' === $typenow )
1004 1476 {
1005 - 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 ) )
1006 1479 {
1007 - $contact_type = ph_clean($_GET['_contact_type']);
1008 1480 if ( $contact_type == 'hotapplicant' )
1009 1481 {
1010 1482 $contact_type = 'applicant';
1011 1483
@@ -1019,34 +1491,63 @@
1019 1491 'value' => $contact_type,
1020 1492 'compare' => 'LIKE'
1021 1493 );
1022 1494 }
1495 +
1496 + $vars = $this->filter_by_date_range($vars, 'date_query');
1023 1497 }
1024 - elseif ( 'enquiry' === $typenow )
1498 + elseif ( 'enquiry' === $typenow )
1025 1499 {
1026 - 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 +
1027 1503 $vars['meta_query'][] = array(
1028 1504 'key' => '_status',
1029 - 'value' => sanitize_text_field( $_GET['_status'] ),
1505 + 'value' => $status,
1030 1506 );
1031 1507 }
1032 - 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 ) ) {
1033 1521 $vars['meta_query'][] = array(
1034 1522 'key' => '_source',
1035 - 'value' => sanitize_text_field( $_GET['_source'] ),
1523 + 'value' => $source,
1036 1524 );
1037 1525 }
1526 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only admin list display or query; no state change.
1038 1527 if ( ! empty( $_GET['_office_id'] ) ) {
1039 1528 $vars['meta_query'][] = array(
1040 1529 'key' => '_office_id',
1041 - 'value' => sanitize_text_field( $_GET['_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'],
1042 1532 );
1043 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');
1044 1544 }
1045 - elseif ( 'appraisal' === $typenow )
1545 + elseif ( 'appraisal' === $typenow )
1046 1546 {
1047 - if ( ! empty( $_GET['_status'] ) ) {
1048 - 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 )
1049 1550 {
1050 1551 case "confirmed":
1051 1552 {
1052 1553 $vars['meta_query'][] = array(
@@ -1074,134 +1575,298 @@
1074 1575 default:
1075 1576 {
1076 1577 $vars['meta_query'][] = array(
1077 1578 'key' => '_status',
1078 - 'value' => sanitize_text_field( $_GET['_status'] ),
1579 + 'value' => $status,
1079 1580 );
1080 1581 }
1081 1582 }
1082 1583 }
1584 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only admin list display or query; no state change.
1083 1585 if ( ! empty( $_GET['_negotiator_id'] ) )
1084 1586 {
1085 1587 $vars['meta_query'][] = array(
1086 1588 'key' => '_negotiator_id',
1589 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only admin list display or query; no state change.
1087 1590 'value' => (int)$_GET['_negotiator_id'],
1088 1591 );
1089 1592 }
1593 +
1594 + $vars = $this->filter_by_date_range($vars);
1090 1595 }
1091 1596 elseif ( 'viewing' === $typenow )
1092 1597 {
1093 - if ( ! empty( $_GET['_status'] ) ) {
1094 - 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 )
1095 1648 {
1096 - case "confirmed":
1097 - {
1649 + case 'pending' :
1098 1650 $vars['meta_query'][] = array(
1099 - 'key' => '_status',
1100 - 'value' => 'pending',
1651 + 'key' => '_start_date',
1652 + 'value' => gmdate('Y-m-d'),
1653 + 'type' => 'date',
1654 + 'compare' => '>',
1101 1655 );
1656 + break;
1657 +
1658 + case 'current' :
1102 1659 $vars['meta_query'][] = array(
1103 - 'key' => '_all_confirmed',
1104 - '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 + )
1105 1688 );
1106 1689 break;
1107 - }
1108 - case "unconfirmed":
1109 - {
1690 +
1691 + case 'finished':
1110 1692 $vars['meta_query'][] = array(
1111 - 'key' => '_status',
1112 - 'value' => 'pending',
1693 + 'key' => '_end_date',
1694 + 'value' => gmdate('Y-m-d'),
1695 + 'type' => 'date',
1696 + 'compare' => '<',
1113 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':
1114 1722 $vars['meta_query'][] = array(
1115 - 'key' => '_all_confirmed',
1116 - 'value' => '',
1723 + 'key' => '_key_date_status',
1724 + 'value' => $value,
1117 1725 );
1118 1726 break;
1119 - }
1120 - case "feedback_passed_on":
1121 - {
1727 + case 'pending':
1122 1728 $vars['meta_query'][] = array(
1123 - 'key' => '_status',
1124 - 'value' => 'carried_out',
1729 + 'key' => '_key_date_status',
1730 + 'value' => 'pending',
1125 1731 );
1732 + break;
1733 + case 'overdue':
1126 1734 $vars['meta_query'][] = array(
1127 - 'key' => '_feedback_status',
1128 - 'value' => array('interested', 'not_interested'),
1735 + 'key' => '_key_date_status',
1736 + 'value' => array('pending', 'booked'),
1129 1737 'compare' => 'IN'
1130 1738 );
1131 1739 $vars['meta_query'][] = array(
1132 - 'key' => '_feedback_passed_on',
1133 - 'value' => 'yes',
1740 + 'key' => '_date_due',
1741 + 'value' => gmdate("Y-m-d"),
1742 + 'type' => 'date',
1743 + 'compare' => '<',
1134 1744 );
1135 1745 break;
1136 - }
1137 - case "feedback_not_passed_on":
1138 - {
1139 - $vars['meta_query'][] = array(
1140 - 'key' => '_status',
1141 - 'value' => 'carried_out',
1142 - );
1746 + case 'upcoming_and_overdue':
1143 1747 $vars['meta_query'][] = array(
1144 - 'key' => '_feedback_status',
1145 - 'value' => array('interested', 'not_interested'),
1748 + 'key' => '_key_date_status',
1749 + 'value' => array('pending', 'booked'),
1146 1750 'compare' => 'IN'
1147 1751 );
1752 + $upcoming_threshold = new DateTime('+ ' . apply_filters( 'propertyhive_key_date_upcoming_days', 7 ) . ' DAYS');
1148 1753 $vars['meta_query'][] = array(
1149 - 'key' => '_feedback_passed_on',
1150 - 'value' => '',
1754 + 'key' => '_date_due',
1755 + 'value' => $upcoming_threshold->format('Y-m-d'),
1756 + 'type' => 'date',
1757 + 'compare' => '<=',
1151 1758 );
1152 1759 break;
1153 - }
1154 - default:
1155 - {
1156 - $vars['meta_query'][] = array(
1157 - 'key' => '_status',
1158 - 'value' => sanitize_text_field( $_GET['_status'] ),
1159 - );
1160 - }
1161 1760 }
1162 1761 }
1163 - 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'] ) )
1164 1765 {
1165 1766 $vars['meta_query'][] = array(
1166 - 'key' => '_negotiator_id',
1167 - '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'],
1168 1770 );
1169 1771 }
1772 +
1773 + $vars = $this->filter_by_date_range($vars, '_date_due');
1170 1774 }
1171 - elseif ( 'offer' === $typenow )
1172 - {
1173 - if ( ! empty( $_GET['_status'] ) ) {
1174 - $vars['meta_query'][] = array(
1175 - 'key' => '_status',
1176 - 'value' => sanitize_text_field( $_GET['_status'] ),
1775 +
1776 + $vars = apply_filters( 'propertyhive_property_filter_query', $vars, $typenow );
1777 +
1778 + return $vars;
1779 + }
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',
1177 1801 );
1178 1802 }
1179 - }
1180 - elseif ( 'sale' === $typenow )
1181 - {
1182 - if ( ! empty( $_GET['_status'] ) ) {
1183 - $vars['meta_query'][] = array(
1184 - 'key' => '_status',
1185 - 'value' => sanitize_text_field( $_GET['_status'] ),
1186 - );
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 + ));
1187 1820 }
1188 - }
1821 + }
1189 1822
1190 - $vars = apply_filters( 'propertyhive_property_filter_query', $vars, $typenow );
1191 -
1192 - return $vars;
1823 + return $vars;
1193 1824 }
1194 1825
1195 - public function posts_join( $join ) {
1826 + public function posts_join( $join, $q ) {
1196 1827 global $typenow, $wp_query, $wpdb;
1197 1828
1198 - if ( !isset($_GET['s']) || ( isset($_GET['s']) && ph_clean($_GET['s']) == '' ) )
1829 + if ( !$q->is_main_query() )
1199 1830 return $join;
1200 1831
1201 - if ( 'appraisal' === $typenow )
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 )
1202 1839 {
1203 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 .= "
1204 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'
1205 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'
1206 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'
1207 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'
@@ -1208,20 +1873,15 @@
1208 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'
1209 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'
1210 1875 ";
1211 1876 }
1212 - elseif ( 'viewing' === $typenow || 'offer' === $typenow || 'sale' === $typenow )
1877 + elseif ( 'viewing' === $typenow || 'offer' === $typenow || 'sale' === $typenow || 'tenancy' === $typenow )
1213 1878 {
1214 1879 $join .= "
1215 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'
1216 1881 LEFT JOIN " . $wpdb->posts . " AS ph_property_filter_posts ON ph_property_filter_posts.ID = ph_property_filter_meta.meta_value
1217 -LEFT JOIN " . $wpdb->postmeta . " AS ph_property_filter_meta_name_number ON ph_property_filter_posts.ID = ph_property_filter_meta_name_number.post_id AND ph_property_filter_meta_name_number.meta_key = '_address_name_number'
1218 -LEFT JOIN " . $wpdb->postmeta . " AS ph_property_filter_meta_street ON ph_property_filter_posts.ID = ph_property_filter_meta_street.post_id AND ph_property_filter_meta_street.meta_key = '_address_street'
1219 -LEFT JOIN " . $wpdb->postmeta . " AS ph_property_filter_meta_2 ON ph_property_filter_posts.ID = ph_property_filter_meta_2.post_id AND ph_property_filter_meta_2.meta_key = '_address_2'
1220 -LEFT JOIN " . $wpdb->postmeta . " AS ph_property_filter_meta_3 ON ph_property_filter_posts.ID = ph_property_filter_meta_3.post_id AND ph_property_filter_meta_3.meta_key = '_address_3'
1221 -LEFT JOIN " . $wpdb->postmeta . " AS ph_property_filter_meta_4 ON ph_property_filter_posts.ID = ph_property_filter_meta_4.post_id AND ph_property_filter_meta_4.meta_key = '_address_4'
1222 -LEFT JOIN " . $wpdb->postmeta . " AS ph_property_filter_meta_postcode ON ph_property_filter_posts.ID = ph_property_filter_meta_postcode.post_id AND ph_property_filter_meta_postcode.meta_key = '_address_postcode'
1223 -
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'
1224 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'
1225 1885 LEFT JOIN " . $wpdb->posts . " AS ph_applicant_filter_posts ON ph_applicant_filter_posts.ID = ph_applicant_filter_meta.meta_value
1226 1886 ";
1227 1887 }
@@ -1228,59 +1888,131 @@
1228 1888
1229 1889 return $join;
1230 1890 }
1231 1891
1232 - public function posts_where( $where ) {
1892 + public function posts_where( $where, $q ) {
1233 1893 global $typenow, $wp_query, $wpdb;
1234 1894
1235 - if ( !isset($_GET['s']) || ( isset($_GET['s']) && ph_clean($_GET['s']) == '' ) )
1895 + if ( !$q->is_main_query() )
1236 1896 return $where;
1237 1897
1238 - if ( 'appraisal' === $typenow )
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 )
1239 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 +
1240 1925 $where = preg_replace(
1241 - "/\(\s*" . $wpdb->posts . ".post_title\s+LIKE\s*(\'[^\']+\')\s*\)/",
1242 - "(
1243 - (" . $wpdb->posts . ".post_title LIKE $1)
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] . ")
1244 1952 OR
1245 - (ph_appraisal_filter_meta_name_number.meta_value LIKE $1)
1953 + (ph_contact_filter_meta_address_concatenated.meta_value LIKE " . $matches[1] . ")
1246 1954 OR
1247 - (ph_appraisal_filter_meta_street.meta_value LIKE $1)
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] . ")
1248 1983 OR
1249 - (ph_appraisal_filter_meta_2.meta_value LIKE $1)
1984 + (ph_appraisal_filter_meta_street.meta_value LIKE " . $matches[1] . ")
1250 1985 OR
1251 - (ph_appraisal_filter_meta_3.meta_value LIKE $1)
1986 + (ph_appraisal_filter_meta_2.meta_value LIKE " . $matches[1] . ")
1252 1987 OR
1253 - (ph_appraisal_filter_meta_4.meta_value LIKE $1)
1988 + (ph_appraisal_filter_meta_3.meta_value LIKE " . $matches[1] . ")
1254 1989 OR
1255 - (ph_appraisal_filter_meta_postcode.meta_value LIKE $1)
1256 - )",
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 + },
1257 1995 $where
1258 1996 );
1259 1997 }
1260 - elseif ( 'viewing' === $typenow || 'offer' === $typenow || 'sale' === $typenow )
1998 + elseif ( 'viewing' === $typenow || 'offer' === $typenow || 'sale' === $typenow || 'tenancy' === $typenow )
1261 1999 {
1262 - $where = preg_replace(
1263 - "/\(\s*" . $wpdb->posts . ".post_title\s+LIKE\s*(\'[^\']+\')\s*\)/",
1264 - "(
1265 - (" . $wpdb->posts . ".post_title LIKE $1)
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] . ")
1266 2005 OR
1267 - (ph_property_filter_posts.post_title LIKE $1)
2006 + (ph_property_filter_posts.post_title LIKE " . $matches[1] . ")
1268 2007 OR
1269 - (ph_property_filter_meta_name_number.meta_value LIKE $1)
2008 + (ph_property_filter_meta_address_concatenated.meta_value LIKE " . $matches[1] . ")
1270 2009 OR
1271 - (ph_property_filter_meta_street.meta_value LIKE $1)
1272 - OR
1273 - (ph_property_filter_meta_2.meta_value LIKE $1)
1274 - OR
1275 - (ph_property_filter_meta_3.meta_value LIKE $1)
1276 - OR
1277 - (ph_property_filter_meta_4.meta_value LIKE $1)
1278 - OR
1279 - (ph_property_filter_meta_postcode.meta_value LIKE $1)
2010 + (ph_property_filter_meta_reference_number.meta_value = " . $reference_exact . ")
1280 2011 OR
1281 - (ph_applicant_filter_posts.post_title LIKE $1)
1282 - )",
2012 + (ph_applicant_filter_posts.post_title LIKE " . $matches[1] . ")
2013 + )";
2014 + },
1283 2015 $where
1284 2016 );
1285 2017 }
1286 2018
@@ -1351,5 +2083,5 @@
1351 2083 }
1352 2084
1353 2085 endif;
1354 2086
1355 -return new PH_Admin_Post_Types();
2087 +return new PH_Admin_Post_Types();