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