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