PluginProbe
Property Hive / 2.3.1
Property Hive v2.3.1
2.3.1 2.3.0 2.2.6 2.2.5 2.2.4 2.2.3 2.2.2 1.4.46 1.4.47 1.4.48 1.4.49 1.4.5 1.4.50 1.4.51 1.4.52 1.4.53 1.4.54 1.4.55 1.4.56 1.4.57 1.4.58 1.4.59 1.4.6 1.4.60 1.4.61 All 261 releases
← All changes | includes/admin/class-ph-admin-post-types.php +1131 -300 1.4.512.3.1 View file →
@@ -1,5 +1,8 @@
1 1 <?php
2 +// phpcs:set WordPress.Security.ValidatedSanitizedInput customSanitizingFunctions[] ph_clean
3 +// ph_clean() recursively sanitizes text; presence, shape and unslashing checks remain separate.
4 +
2 5 /**
3 6 * Post Types Admin
4 7 *
5 8 * @author PropertyHive
@@ -14,8 +17,9 @@
14 17
15 18 /**
16 19 * PH_Admin_Post_Types Class
17 20 */
21 +// phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedClassFound -- Legacy public global class PH_Admin_Post_Types; preserving the existing PH_* class name is required for plugin and extension compatibility.
18 22 class PH_Admin_Post_Types {
19 23
20 24 /**
21 25 * Constructor
@@ -22,8 +26,9 @@
22 26 */
23 27 public function __construct() {
24 28 add_action( 'admin_init', array( $this, 'include_post_type_handlers' ) );
25 29 add_filter( 'post_updated_messages', array( $this, 'post_updated_messages' ) );
30 + add_action( 'pre_get_posts', array( $this, 'refresh_property_office_filtering' ));
26 31 add_action( 'admin_print_scripts', array( $this, 'remove_month_filter' ) );
27 32 add_action( 'admin_print_scripts', array( $this, 'disable_autosave' ) );
28 33
29 34 // Filters
@@ -28,17 +33,277 @@
28 33
29 34 // Filters
30 35 add_action( 'restrict_manage_posts', array( $this, 'restrict_manage_posts' ) );
31 36 add_filter( 'request', array( $this, 'request_query' ) );
37 + add_filter( 'posts_join', array( $this, 'posts_join' ), 10, 2 );
38 + add_filter( 'posts_where', array( $this, 'posts_where' ), 10, 2 );
32 39
33 40 // Status transitions
34 41 add_action( 'delete_post', array( $this, 'delete_post' ) );
35 42 add_action( 'wp_trash_post', array( $this, 'trash_post' ) );
36 43 add_action( 'untrash_post', array( $this, 'untrash_post' ) );
44 +
45 + add_action( 'admin_init', array( $this, 'handle_archive_action' ) );
46 + add_action( 'admin_init', array( $this, 'handle_unarchive_action' ) );
47 +
48 + $post_types = array('property', 'contact', 'appraisal', 'viewing', 'offer', 'sale', 'tenancy', 'key_date');
49 + $post_types = apply_filters( 'propertyhive_post_types_with_archive', $post_types );
50 +
51 + foreach ( $post_types as $post_type )
52 + {
53 + add_filter( 'views_edit-' . $post_type, array( $this, 'adjust_post_status_views' ) );
54 + add_filter( "bulk_actions-edit-$post_type", array( $this, 'register_bulk_action_move_to_archive' ) );
55 + add_filter( "handle_bulk_actions-edit-$post_type", array( $this, 'handle_bulk_action_archive_and_unarchive' ), 10, 3 );
56 + }
57 +
58 + add_filter( 'post_row_actions', array( $this, 'modify_post_row_actions_for_archived' ), 10, 2 );
59 + }
60 +
61 + /**
62 + * Read one scalar admin query value after WordPress unslashes and sanitizes it.
63 + *
64 + * Admin list filters are read-only, but their values still flow into markup and
65 + * query arguments. Returning an empty value for arrays keeps scalar filters
66 + * from accidentally accepting a malformed request while preserving the
67 + * existing empty-filter behaviour.
68 + *
69 + * @param string $key Query-string key.
70 + * @return string
71 + */
72 + private function get_admin_query_value( $key ) {
73 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only admin list display or query; no state change.
74 + if ( ! isset( $_GET[ $key ] ) || ! is_scalar( $_GET[ $key ] ) ) {
75 + return '';
76 + }
77 +
78 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Read-only admin list value is copied, unslashed immediately below, and sanitized before use; the sniffer reports the source assignment instead of the sanitization boundary.
79 + $raw_value = $_GET[ $key ];
80 + $raw_value = wp_unslash( (string) $raw_value );
81 +
82 + return sanitize_text_field( $raw_value );
83 + }
84 +
85 + public function handle_bulk_action_archive_and_unarchive($redirect_to, $doaction, $post_ids)
86 + {
87 + if ($doaction === 'move_to_archive')
88 + {
89 + foreach ($post_ids as $post_id)
90 + {
91 + // Check permissions
92 + if (!current_user_can('edit_post', $post_id)) {
93 + continue;
94 + }
95 +
96 + // Update the post status to 'archive'
97 + $updated_post = array(
98 + 'ID' => $post_id,
99 + 'post_status' => 'archive',
100 + );
101 +
102 + wp_update_post($updated_post);
103 + }
104 +
105 + $redirect_to = add_query_arg('bulk_archived_posts', count($post_ids), $redirect_to);
106 + }
107 + elseif ($doaction === 'unarchive')
108 + {
109 + foreach ($post_ids as $post_id)
110 + {
111 + // Check permissions
112 + if (!current_user_can('edit_post', $post_id)) {
113 + continue;
114 + }
115 +
116 + // Update the post status to 'publish' (or whatever the original status should be)
117 + $updated_post = array(
118 + 'ID' => $post_id,
119 + 'post_status' => 'publish',
120 + );
121 +
122 + wp_update_post($updated_post);
123 + }
124 +
125 + $redirect_to = add_query_arg('bulk_unarchived_posts', count($post_ids), $redirect_to);
126 + }
127 +
128 + return $redirect_to;
129 + }
130 +
131 + public function register_bulk_action_move_to_archive( $bulk_actions )
132 + {
133 + global $post_status;
134 +
135 + // Define our custom actions
136 + $custom_actions = array();
137 +
138 + if ($post_status === 'archive') {
139 + $custom_actions['unarchive'] = __('Unarchive', 'propertyhive');
140 + } else {
141 + $custom_actions['move_to_archive'] = __('Move to Archive', 'propertyhive');
142 + }
143 +
144 + // Check if 'trash' exists and insert custom actions before it
145 + if (isset($bulk_actions['trash']))
146 + {
147 + $new_actions = array();
148 + foreach ($bulk_actions as $key => $value) {
149 + if ($key === 'trash') {
150 + $new_actions = array_merge($new_actions, $custom_actions);
151 + }
152 + $new_actions[$key] = $value;
153 + }
154 + return $new_actions;
155 + }
156 + elseif (isset($bulk_actions['untrash']))
157 + {
158 + $new_actions = array();
159 + foreach ($bulk_actions as $key => $value) {
160 + if ($key === 'untrash') {
161 + $new_actions = array_merge($new_actions, $custom_actions);
162 + }
163 + $new_actions[$key] = $value;
164 + }
165 + return $new_actions;
166 + }
167 + else
168 + {
169 + // If 'trash' doesn't exist, append custom actions at the end
170 + return array_merge($bulk_actions, $custom_actions);
171 + }
172 + }
173 +
174 + public function modify_post_row_actions_for_archived( $actions, $post )
175 + {
176 + // Define the post types that can be archived
177 + $post_types = array('property', 'contact', 'appraisal', 'viewing', 'offer', 'sale', 'tenancy', 'key_date');
178 + $post_types = apply_filters('propertyhive_post_types_with_archive', $post_types);
179 +
180 + // Check if the current post type is in the allowed post types and if the post is archived
181 + if ( in_array($post->post_type, $post_types) && $post->post_status == 'archive' )
182 + {
183 + // Remove the "View" link
184 + if (isset($actions['view'])) {
185 + unset($actions['view']);
186 + }
187 +
188 + // Add the "Unarchive" link
189 + $unarchive_url = wp_nonce_url(admin_url('post.php?post=' . $post->ID . '&action=unarchive&return=archive'), 'unarchive-post_' . $post->ID);
190 + $actions['unarchive'] = '<a href="' . esc_url($unarchive_url) . '">' . __('Unarchive', 'propertyhive') . '</a>';
191 + }
192 +
193 + return $actions;
194 + }
195 +
196 + public function adjust_post_status_views( $views )
197 + {
198 + if (isset($views['archive']))
199 + {
200 + $archive = $views['archive'];
201 + unset($views['archive']);
202 +
203 + $new_views = array();
204 + $bin_exists = false;
205 +
206 + foreach ($views as $key => $view) {
207 + if ($key === 'trash') {
208 + $bin_exists = true;
209 + $new_views['archive'] = $archive;
210 + }
211 + $new_views[$key] = $view;
212 + }
213 +
214 + // Ensure 'archive' is added to the end if 'trash' is not present
215 + if (!$bin_exists) {
216 + $new_views['archive'] = $archive;
217 + }
218 +
219 + return $new_views;
220 + }
221 +
222 + return $views;
223 + }
224 +
225 + public function handle_archive_action()
226 + {
227 + // Check if the action and nonce are set and valid
228 + if ( !isset($_GET['action']) || $_GET['action'] !== 'archive_single' )
229 + return;
37 230
231 + $post_id = isset($_GET['post']) ? intval($_GET['post']) : 0;
232 + $post_type = get_post_type($post_id);
233 +
234 + if ( !wp_verify_nonce( ( isset( $_GET['_wpnonce'] ) && is_string( $_GET['_wpnonce'] ) ) ? sanitize_text_field( wp_unslash( $_GET['_wpnonce'] ) ) : '', 'archive-post_' . $post_id) )
235 + {
236 + wp_die(esc_html(__('Security check failed.', 'propertyhive')));
237 + }
238 +
239 + if ( !current_user_can('edit_post', $post_id) )
240 + {
241 + wp_die(esc_html(__('You do not have permission to edit this post.', 'propertyhive')));
242 + }
243 +
244 + // Update the post status to 'archive'
245 + $updated_post = array(
246 + 'ID' => $post_id,
247 + 'post_status' => 'archive',
248 + );
249 +
250 + $result = wp_update_post($updated_post, true);
251 +
252 + if ( is_wp_error($result) )
253 + {
254 + wp_die(esc_html(__('An error occurred while archiving the post.', 'propertyhive')));
255 + }
256 +
257 + // Redirect to the main list of contacts
258 + wp_safe_redirect(admin_url('edit.php?post_type=' . $post_type));
259 + exit;
260 + }
261 +
262 + public function handle_unarchive_action()
263 + {
264 + // Check if the action and nonce are set and valid
265 + if ( !isset($_GET['action']) || $_GET['action'] !== 'unarchive_single' )
266 + return;
38 267
39 - }
268 + $post_id = isset($_GET['post']) ? intval($_GET['post']) : 0;
269 + $post_type = get_post_type($post_id);
40 270
271 + if ( !wp_verify_nonce( ( isset( $_GET['_wpnonce'] ) && is_string( $_GET['_wpnonce'] ) ) ? sanitize_text_field( wp_unslash( $_GET['_wpnonce'] ) ) : '', 'unarchive-post_' . $post_id) )
272 + {
273 + wp_die(esc_html(__('Security check failed.', 'propertyhive')));
274 + }
275 +
276 + if ( !current_user_can('edit_post', $post_id) )
277 + {
278 + wp_die(esc_html(__('You do not have permission to edit this post.', 'propertyhive')));
279 + }
280 +
281 + // Update the post status to 'publish'
282 + $updated_post = array(
283 + 'ID' => $post_id,
284 + 'post_status' => 'publish',
285 + );
286 +
287 + $result = wp_update_post($updated_post, true);
288 +
289 + if ( is_wp_error($result) )
290 + {
291 + wp_die(esc_html(__('An error occurred while unarchiving the post.', 'propertyhive')));
292 + }
293 +
294 + // Redirect to the main list of contacts
295 + if ( isset($_GET['return']) && $_GET['return'] === 'archive' )
296 + {
297 + wp_safe_redirect(admin_url('edit.php?post_status=archive&post_type=' . get_post_type($post_id)));
298 + }
299 + else
300 + {
301 + wp_safe_redirect(admin_url('edit.php?post_type=' . get_post_type($post_id)));
302 + }
303 + exit;
304 + }
305 +
41 306 /**
42 307 * Conditonally load classes and functions only needed when viewing a post type.
43 308 */
44 309 public function include_post_type_handlers() {
@@ -52,8 +317,10 @@
52 317 include( 'post-types/class-ph-admin-cpt-appraisal.php' );
53 318 include( 'post-types/class-ph-admin-cpt-viewing.php' );
54 319 include( 'post-types/class-ph-admin-cpt-offer.php' );
55 320 include( 'post-types/class-ph-admin-cpt-sale.php' );
321 + include( 'post-types/class-ph-admin-cpt-tenancy.php' );
322 + include( 'post-types/class-ph-admin-cpt-key-date.php' );
56 323 }
57 324
58 325 /**
59 326 * Change messages when a post type is updated.
@@ -65,19 +332,24 @@
65 332 global $post, $post_ID;
66 333
67 334 $messages['property'] = array(
68 335 0 => '', // Unused. Messages start at index 1.
69 - 1 => sprintf( __( 'Property updated. <a href="%s">View Property</a>', 'propertyhive' ), esc_url( get_permalink($post_ID) ) ),
336 + /* translators: %s: URL to view the property */
337 + 1 => sprintf( __( 'Property updated. <a href="%s">View property</a>', 'propertyhive' ), esc_url( get_permalink($post_ID) ) ),
70 338 2 => __( 'Custom field updated.', 'propertyhive' ),
71 339 3 => __( 'Custom field deleted.', 'propertyhive' ),
72 340 4 => __( 'Property updated.', 'propertyhive' ),
73 - 5 => isset($_GET['revision']) ? sprintf( __( 'Property restored to revision from %s', 'propertyhive' ), wp_post_revision_title( (int) $_GET['revision'], false ) ) : false,
74 - 6 => sprintf( __( 'Property published. <a href="%s">View Property</a>', 'propertyhive' ), esc_url( get_permalink($post_ID) ) ),
341 + 5 => __( 'Revision restored.', 'propertyhive' ),
342 + /* translators: %s: URL to view the property */
343 + 6 => sprintf( __( 'Property published. <a href="%s">View property</a>', 'propertyhive' ), esc_url( get_permalink($post_ID) ) ),
75 344 7 => __( 'Property saved.', 'propertyhive' ),
76 - 8 => sprintf( __( 'Property submitted. <a target="_blank" href="%s">Preview Property</a>', 'propertyhive' ), esc_url( add_query_arg( 'preview', 'true', get_permalink($post_ID) ) ) ),
77 - 9 => sprintf( __( 'Property scheduled for: <strong>%1$s</strong>. <a target="_blank" href="%2$s">Preview Property</a>', 'propertyhive' ),
345 + /* translators: %s: URL to preview the property */
346 + 8 => sprintf( __( 'Property submitted. <a target="_blank" href="%s">Preview property</a>', 'propertyhive' ), esc_url( add_query_arg( 'preview', 'true', get_permalink($post_ID) ) ) ),
347 + /* translators: 1: formatted date, 2: URL to preview the property */
348 + 9 => sprintf( __( 'Property scheduled for: <strong>%1$s</strong>. <a target="_blank" href="%2$s">Preview property</a>', 'propertyhive' ),
78 349 date_i18n( __( 'M j, Y @ G:i', 'propertyhive' ), strtotime( $post->post_date ) ), esc_url( get_permalink($post_ID) ) ),
79 - 10 => sprintf( __( 'Property draft updated. <a target="_blank" href="%s">Preview Property</a>', 'propertyhive' ), esc_url( add_query_arg( 'preview', 'true', get_permalink($post_ID) ) ) ),
350 + /* translators: %s: URL to preview the property */
351 + 10 => sprintf( __( 'Property draft updated. <a target="_blank" href="%s">Preview property</a>', 'propertyhive' ), esc_url( add_query_arg( 'preview', 'true', get_permalink($post_ID) ) ) ),
80 352 );
81 353
82 354 $messages['contact'] = array(
83 355 0 => '', // Unused. Messages start at index 1.
@@ -84,12 +356,13 @@
84 356 1 => __( 'Contact updated.', 'propertyhive' ),
85 357 2 => __( 'Custom field updated.', 'propertyhive' ),
86 358 3 => __( 'Custom field deleted.', 'propertyhive' ),
87 359 4 => __( 'Contact updated.', 'propertyhive' ),
88 - 5 => isset($_GET['revision']) ? sprintf( __( 'Contact restored to revision from %s', 'propertyhive' ), wp_post_revision_title( (int) $_GET['revision'], false ) ) : false,
360 + 5 => __( 'Revision restored.', 'propertyhive' ),
89 361 6 => __( 'Contact published.', 'propertyhive' ),
90 362 7 => __( 'Contact saved.', 'propertyhive' ),
91 363 8 => __( 'Contact submitted.', 'propertyhive' ),
364 + /* translators: 1: formatted date */
92 365 9 => sprintf( __( 'Contact scheduled for: <strong>%1$s</strong>.', 'propertyhive' ), date_i18n( __( 'M j, Y @ G:i', 'propertyhive' ), strtotime( $post->post_date ) )),
93 366 10 => __( 'Contact draft updated.', 'propertyhive' ),
94 367 );
95 368
@@ -98,12 +371,13 @@
98 371 1 => __( 'Office updated.', 'propertyhive' ),
99 372 2 => __( 'Custom field updated.', 'propertyhive' ),
100 373 3 => __( 'Custom field deleted.', 'propertyhive' ),
101 374 4 => __( 'Office updated.', 'propertyhive' ),
102 - 5 => isset($_GET['revision']) ? sprintf( __( 'Office restored to revision from %s', 'propertyhive' ), wp_post_revision_title( (int) $_GET['revision'], false ) ) : false,
375 + 5 => __( 'Revision restored.', 'propertyhive' ),
103 376 6 => sprintf( __( 'Office published.', 'propertyhive' ), esc_url( get_permalink($post_ID) ) ),
104 377 7 => __( 'Office saved.', 'propertyhive' ),
105 378 8 => sprintf( __( 'Office submitted.', 'propertyhive' ), esc_url( add_query_arg( 'preview', 'true', get_permalink($post_ID) ) ) ),
379 + /* translators: 1: formatted date */
106 380 9 => sprintf( __( 'Office scheduled for: <strong>%1$s</strong>.', 'propertyhive' ),
107 381 date_i18n( __( 'M j, Y @ G:i', 'propertyhive' ), strtotime( $post->post_date ) ), esc_url( get_permalink($post_ID) ) ),
108 382 10 => sprintf( __( 'Office draft updated. ', 'propertyhive' ), esc_url( add_query_arg( 'preview', 'true', get_permalink($post_ID) ) ) ),
109 383 );
@@ -113,12 +387,13 @@
113 387 1 => sprintf( __( 'Enquiry updated.', 'propertyhive' ), esc_url( get_permalink($post_ID) ) ),
114 388 2 => __( 'Custom field updated.', 'propertyhive' ),
115 389 3 => __( 'Custom field deleted.', 'propertyhive' ),
116 390 4 => __( 'Enquiry updated.', 'propertyhive' ),
117 - 5 => isset($_GET['revision']) ? sprintf( __( 'Enquiry restored to revision from %s', 'propertyhive' ), wp_post_revision_title( (int) $_GET['revision'], false ) ) : false,
391 + 5 => __( 'Revision restored.', 'propertyhive' ),
118 392 6 => sprintf( __( 'Enquiry published.', 'propertyhive' ), esc_url( get_permalink($post_ID) ) ),
119 393 7 => __( 'Enquiry saved.', 'propertyhive' ),
120 394 8 => sprintf( __( 'Enquiry submitted.', 'propertyhive' ), esc_url( add_query_arg( 'preview', 'true', get_permalink($post_ID) ) ) ),
395 + /* translators: 1: formatted date */
121 396 9 => sprintf( __( 'Enquiry scheduled for: <strong>%1$s</strong>.', 'propertyhive' ),
122 397 date_i18n( __( 'M j, Y @ G:i', 'propertyhive' ), strtotime( $post->post_date ) ), esc_url( get_permalink($post_ID) ) ),
123 398 10 => sprintf( __( 'Enquiry draft updated.', 'propertyhive' ), esc_url( add_query_arg( 'preview', 'true', get_permalink($post_ID) ) ) ),
124 399 );
@@ -130,10 +405,13 @@
130 405 * Remove month filter from some property hive pages
131 406 */
132 407 public function remove_month_filter() {
133 408 global $typenow;
134 -
135 - if ($typenow == 'property' || $typenow == 'contact' || $typenow == 'appraisal' || $typenow == 'viewing' || $typenow == 'offer' || $typenow == 'sale')
409 +
410 + $post_types_to_hide_months_dropdown = array('property', 'contact', 'enquiry', 'appraisal', 'viewing', 'offer', 'sale', 'tenancy', 'key_date');
411 + $post_types_to_hide_months_dropdown = apply_filters( 'propertyhive_post_types_to_hide_months_dropdown', $post_types_to_hide_months_dropdown );
412 +
413 + if ( in_array($typenow, $post_types_to_hide_months_dropdown) )
136 414 {
137 415 add_filter('months_dropdown_results', '__return_empty_array');
138 416 }
139 417 }
@@ -179,8 +457,14 @@
179 457 break;
180 458 case 'sale' :
181 459 $this->sale_filters();
182 460 break;
461 + case 'tenancy' :
462 + $this->tenancy_filters();
463 + break;
464 + case 'key_date' :
465 + $this->key_date_filters();
466 + break;
183 467 default :
184 468 break;
185 469 }
186 470 }
@@ -198,10 +482,11 @@
198 482 $output .= $this->property_marketing_filter();
199 483 $output .= $this->property_availability_filter();
200 484 $output .= $this->property_location_filter();
201 485 $output .= $this->property_office_filter();
202 - $output .= $this->property_negotiator_filter();
486 + $output .= $this->negotiator_filter();
203 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.
204 489 echo apply_filters( 'propertyhive_property_filters', $output );
205 490 }
206 491
207 492 /**
@@ -211,22 +496,24 @@
211 496 global $wp_query;
212 497
213 498 $departments = ph_get_departments();
214 499
215 - $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 : '';
216 503
217 504 // Department filtering
218 505 $output = '<select name="_department" id="dropdown_property_department">';
219 506
220 - $output .= '<option value="">' . __( 'All Departments', 'propertyhive' ) . '</option>';
507 + $output .= '<option value="">' . esc_html__( 'All Departments', 'propertyhive' ) . '</option>';
221 508
222 509 foreach ( $departments as $key => $value )
223 510 {
224 511 if ( get_option( 'propertyhive_active_departments_' . str_replace("residential-", "", $key) ) == 'yes' )
225 512 {
226 - $output .= '<option value="' . $key . '"';
513 + $output .= '<option value="' . esc_attr($key) . '"';
227 514 $output .= selected( $key, $selected_department, false );
228 - $output .= '>' . $value . '</option>';
515 + $output .= '>' . esc_html($value) . '</option>';
229 516 }
230 517 }
231 518
232 519 $output .= '</select>';
@@ -242,9 +529,9 @@
242 529
243 530 // Department filtering
244 531 $output = '<select name="_office_id" id="dropdown_property_office_id">';
245 532
246 - $output .= '<option value="">' . __( 'All Offices', 'propertyhive' ) . '</option>';
533 + $output .= '<option value="">' . esc_html__( 'All Offices', 'propertyhive' ) . '</option>';
247 534
248 535 $args = array(
249 536 'post_type' => 'office',
250 537 'nopaging' => true,
@@ -258,14 +545,16 @@
258 545 while ($office_query->have_posts())
259 546 {
260 547 $office_query->the_post();
261 548
262 - $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.
263 551 if ( isset( $_GET['_office_id'] ) && ! empty( $_GET['_office_id'] ) )
264 552 {
553 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only admin list display or query; no state change.
265 554 $output .= selected( $post->ID, (int)$_GET['_office_id'], false );
266 555 }
267 - $output .= '>' . get_the_title() . '</option>';
556 + $output .= '>' . esc_html(get_the_title()) . '</option>';
268 557 }
269 558 }
270 559
271 560 wp_reset_postdata();
@@ -275,32 +564,51 @@
275 564 return $output;
276 565 }
277 566
278 567 /**
279 - * Show a property negotiator filter box
568 + * Show a negotiator filter box
280 569 */
281 - public function property_negotiator_filter() {
282 - global $wp_query, $post;
283 -
284 - $selected = '';
285 - if ( isset( $_GET['_negotiator_id'] ) && ! empty( $_GET['_negotiator_id'] ) )
286 - {
287 - $selected = (int)$_GET['_negotiator_id'];
288 - }
289 -
290 - $args = array(
570 + public function negotiator_filter() {
571 +
572 + return wp_dropdown_users(array(
291 573 'name' => '_negotiator_id',
292 574 'id' => 'dropdown_property_negotiator_id',
293 - 'show_option_all' => __( 'All Negotiators', 'propertyhive' ),
294 - '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'],
295 578 'echo' => false,
296 - 'role__not_in' => array('property_hive_contact')
297 - );
298 - $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 + }
299 583
300 - return $output;
301 - }
584 + /**
585 + * Show a date range selector
586 + */
587 + public function date_range_filter() {
302 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 +
303 611 /**
304 612 * Show a property location filter box
305 613 */
306 614 public function property_location_filter() {
@@ -313,9 +621,9 @@
313 621 $args = array(
314 622 'hide_empty' => false,
315 623 'parent' => 0
316 624 );
317 - $terms = get_terms( 'location', $args );
625 + $terms = get_terms( array_merge( wp_parse_args( $args ), array( 'taxonomy' => 'location' ) ) );
318 626
319 627 if ( !empty( $terms ) && !is_wp_error( $terms ) )
320 628 {
321 629 foreach ($terms as $term)
@@ -325,9 +633,9 @@
325 633 $args = array(
326 634 'hide_empty' => false,
327 635 'parent' => $term->term_id
328 636 );
329 - $subterms = get_terms( 'location', $args );
637 + $subterms = get_terms( array_merge( wp_parse_args( $args ), array( 'taxonomy' => 'location' ) ) );
330 638
331 639 if ( !empty( $subterms ) && !is_wp_error( $subterms ) )
332 640 {
333 641 foreach ($subterms as $term)
@@ -337,9 +645,9 @@
337 645 $args = array(
338 646 'hide_empty' => false,
339 647 'parent' => $term->term_id
340 648 );
341 - $subsubterms = get_terms( 'location', $args );
649 + $subsubterms = get_terms( array_merge( wp_parse_args( $args ), array( 'taxonomy' => 'location' ) ) );
342 650
343 651 if ( !empty( $subsubterms ) && !is_wp_error( $subsubterms ) )
344 652 {
345 653 foreach ($subsubterms as $term)
@@ -351,20 +659,22 @@
351 659 }
352 660 }
353 661 }
354 662
355 - $output .= '<option value="">' . __( 'All Locations', 'propertyhive' ) . '</option>';
663 + $output .= '<option value="">' . esc_html(__( 'All Locations', 'propertyhive' )) . '</option>';
356 664
357 665 if ( !empty($options) )
358 666 {
359 667 foreach ( $options as $value => $label )
360 668 {
361 - $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.
362 671 if ( isset( $_GET['_location_id'] ) && ! empty( $_GET['_location_id'] ) )
363 672 {
673 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only admin list display or query; no state change.
364 674 $output .= selected( $value, (int)$_GET['_location_id'], false );
365 675 }
366 - $output .= '>' . $label . '</option>';
676 + $output .= '>' . esc_html($label) . '</option>';
367 677 }
368 678 }
369 679
370 680 $output .= '</select>';
@@ -385,9 +695,9 @@
385 695 $args = array(
386 696 'hide_empty' => false,
387 697 'parent' => 0
388 698 );
389 - $terms = get_terms( 'availability', $args );
699 + $terms = get_terms( array_merge( wp_parse_args( $args ), array( 'taxonomy' => 'availability' ) ) );
390 700
391 701 if ( !empty( $terms ) && !is_wp_error( $terms ) )
392 702 {
393 703 foreach ($terms as $term)
@@ -395,20 +705,22 @@
395 705 $options[$term->term_id] = $term->name;
396 706 }
397 707 }
398 708
399 - $output .= '<option value="">' . __( 'All Availabilities', 'propertyhive' ) . '</option>';
709 + $output .= '<option value="">' . esc_html(__( 'All Availabilities', 'propertyhive' )) . '</option>';
400 710
401 711 if ( !empty($options) )
402 712 {
403 713 foreach ( $options as $value => $label )
404 714 {
405 - $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.
406 717 if ( isset( $_GET['_availability_id'] ) && ! empty( $_GET['_availability_id'] ) )
407 718 {
719 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only admin list display or query; no state change.
408 720 $output .= selected( $value, (int)$_GET['_availability_id'], false );
409 721 }
410 - $output .= '>' . $label . '</option>';
722 + $output .= '>' . esc_html($label) . '</option>';
411 723 }
412 724 }
413 725
414 726 $output .= '</select>';
@@ -424,9 +736,9 @@
424 736
425 737 // Availability filtering
426 738 $output = '<select name="_marketing" id="dropdown_property_marketing">';
427 739
428 - $output .= '<option value="">' . __( 'All Marketing Statuses', 'propertyhive' ) . '</option>';
740 + $output .= '<option value="">' . esc_html__( 'All Marketing Statuses', 'propertyhive' ) . '</option>';
429 741
430 742 $options = array(
431 743 'on_market' => __( 'On Market Only', 'propertyhive' ),
432 744 'off_market' => __( 'Not On Market Only', 'propertyhive' ),
@@ -436,9 +748,9 @@
436 748 $args = array(
437 749 'hide_empty' => false,
438 750 'parent' => 0
439 751 );
440 - $terms = get_terms( 'marketing_flag', $args );
752 + $terms = get_terms( array_merge( wp_parse_args( $args ), array( 'taxonomy' => 'marketing_flag' ) ) );
441 753
442 754 if ( !empty( $terms ) && !is_wp_error( $terms ) )
443 755 {
444 756 foreach ($terms as $term)
@@ -447,17 +759,18 @@
447 759 }
448 760 }
449 761
450 762 $options = apply_filters( 'propertyhive_property_filter_marketing_options', $options );
763 + $selected_marketing = $this->get_admin_query_value( '_marketing' );
451 764
452 765 foreach ( $options as $key => $value )
453 766 {
454 - $output .= '<option value="' . $key . '"';
455 - if ( isset( $_GET['_marketing'] ) && ! empty( $_GET['_marketing'] ) )
767 + $output .= '<option value="' . esc_attr($key) . '"';
768 + if ( ! empty( $selected_marketing ) )
456 769 {
457 - $output .= selected( $key, sanitize_text_field($_GET['_marketing']), false );
770 + $output .= selected( $key, $selected_marketing, false );
458 771 }
459 - $output .= '>' . $value . '</option>';
772 + $output .= '>' . esc_html($value) . '</option>';
460 773 }
461 774
462 775 $output .= '</select>';
463 776
@@ -469,9 +782,11 @@
469 782 */
470 783 public function contact_filters() {
471 784 global $wp_query;
472 785
473 - $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 : '';
474 789
475 790 // Type filtering
476 791 $options = array();
477 792
@@ -477,9 +792,9 @@
477 792
478 793 // Owners
479 794 $option = '<option value="owner"';
480 795 $option .= selected( 'owner', $selected_contact_type, false );
481 - $option .= '>' . __( 'Owners and Landlords', 'propertyhive' ) . '</option>';
796 + $option .= '>' . esc_html(__( 'Owners and Landlords', 'propertyhive' )) . '</option>';
482 797
483 798 $options[] = $option;
484 799
485 800 // Potential Owners
@@ -484,9 +799,9 @@
484 799
485 800 // Potential Owners
486 801 $option = '<option value="potentialowner"';
487 802 $option .= selected( 'potentialowner', $selected_contact_type, false );
488 - $option .= '>' . __( 'Potential Owners and Landlords', 'propertyhive' ) . '</option>';
803 + $option .= '>' . esc_html(__( 'Potential Owners and Landlords', 'propertyhive' )) . '</option>';
489 804
490 805 $options[] = $option;
491 806
492 807 // Applicants
@@ -491,9 +806,9 @@
491 806
492 807 // Applicants
493 808 $option = '<option value="applicant"';
494 809 $option .= selected( 'applicant', $selected_contact_type, false );
495 - $option .= '>' . __( 'Applicants', 'propertyhive' ) . '</option>';
810 + $option .= '>' . esc_html(__( 'Applicants', 'propertyhive' )) . '</option>';
496 811
497 812 $options[] = $option;
498 813
499 814 // Hot Applicants
@@ -498,9 +813,9 @@
498 813
499 814 // Hot Applicants
500 815 $option = '<option value="hotapplicant"';
501 816 $option .= selected( 'hotapplicant', $selected_contact_type, false );
502 - $option .= '>- ' . __( 'Hot Applicants', 'propertyhive' ) . '</option>';
817 + $option .= '>- ' . esc_html(__( 'Hot Applicants', 'propertyhive' )) . '</option>';
503 818
504 819 $options[] = $option;
505 820
506 821 // Third Parties
@@ -505,9 +820,9 @@
505 820
506 821 // Third Parties
507 822 $option = '<option value="thirdparty"';
508 823 $option .= selected( 'thirdparty', $selected_contact_type, false );
509 - $option .= '>' . __( 'Third Party Contacts', 'propertyhive' ) . '</option>';
824 + $option .= '>' . esc_html(__( 'Third Party Contacts', 'propertyhive' )) . '</option>';
510 825
511 826 $options[] = $option;
512 827
513 828 $options = apply_filters( 'propertyhive_contact_filter_options', $options );
@@ -516,9 +831,9 @@
516 831 if (count($options) > 1)
517 832 {
518 833 $output = '<select name="_contact_type" id="dropdown_contact_type">';
519 834
520 - $output .= '<option value="">' . __( 'Show all contact types', 'propertyhive' ) . '</option>';
835 + $output .= '<option value="">' . esc_html(__( 'Show all contact types', 'propertyhive' )) . '</option>';
521 836
522 837 $output .= implode("", $options);
523 838
524 839 $output .= '</select>';
@@ -523,9 +838,12 @@
523 838
524 839 $output .= '</select>';
525 840 }
526 841
527 - 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 );
528 846 }
529 847
530 848 /**
531 849 * Show an enquiry filter box
@@ -535,12 +853,15 @@
535 853
536 854 // Department filtering
537 855 $output = '';
538 856
857 + $output .= $this->date_range_filter();
539 858 $output .= $this->enquiry_status_filter();
540 859 $output .= $this->enquiry_source_filter();
541 860 $output .= $this->enquiry_office_filter();
861 + $output .= $this->enquiry_negotiator_filter();
542 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.
543 864 echo apply_filters( 'propertyhive_enquiry_filters', $output );
544 865 }
545 866
546 867 /**
@@ -548,21 +869,30 @@
548 869 */
549 870 public function enquiry_status_filter() {
550 871 global $wp_query;
551 872
552 - $selected_status = isset( $_GET['_status'] ) && in_array( $_GET['_status'], array( 'open', 'closed' ) ) ? $_GET['_status'] : '';
553 -
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 +
554 877 // Status filtering
555 - $output = '<select name="_status" id="dropdown_enquiry_status">';
556 -
557 - $output .= '<option value="open"';
558 - $output .= selected( 'open', $selected_status, false );
559 - $output .= '>' . __( 'Open', 'propertyhive' ) . '</option>';
878 + $output = '<select name="_status" id="dropdown_enquiry_status">
879 + <option value="all"' . selected( 'all', $selected_status, false ) . '>All</option>';
560 880
561 - $output .= '<option value="closed"';
562 - $output .= selected( 'closed', $selected_status, false );
563 - $output .= '>' . __( 'Closed', 'propertyhive' ) . '</option>';
564 -
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 +
565 895 $output .= '</select>';
566 896
567 897 return $output;
568 898 }
@@ -578,22 +908,25 @@
578 908 'website' => __( 'Website', 'propertyhive' )
579 909 );
580 910
581 911 $sources = apply_filters( 'propertyhive_enquiry_sources', $sources );
912 +
913 + asort($sources);
582 914
583 915 // Status filtering
584 916 $output = '<select name="_source" id="dropdown_enquiry_source">';
917 + $selected_source = $this->get_admin_query_value( '_source' );
585 918
586 - $output .= '<option value="">' . __( 'Show all sources', 'propertyhive' ) . '</option>';
919 + $output .= '<option value="">' . esc_html__( 'Show all sources', 'propertyhive' ) . '</option>';
587 920
588 921 foreach ( $sources as $key => $value )
589 922 {
590 - $output .= '<option value="' . $key . '"';
591 - if ( isset( $_GET['_source'] ) && ! empty( $_GET['_source'] ) )
923 + $output .= '<option value="' . esc_attr($key) . '"';
924 + if ( ! empty( $selected_source ) )
592 925 {
593 - $output .= selected( $key, sanitize_text_field($_GET['_source']), false );
926 + $output .= selected( $key, $selected_source, false );
594 927 }
595 - $output .= '>' . __( $value, 'propertyhive' ) . '</option>';
928 + $output .= '>' . esc_html( $value ) . '</option>';
596 929 }
597 930
598 931 $output .= '</select>';
599 932
@@ -608,9 +941,9 @@
608 941
609 942 // Department filtering
610 943 $output = '<select name="_office_id" id="dropdown_enquiry_office_id">';
611 944
612 - $output .= '<option value="">' . __( 'All Offices', 'propertyhive' ) . '</option>';
945 + $output .= '<option value="">' . esc_html__( 'All Offices', 'propertyhive' ) . '</option>';
613 946
614 947 $args = array(
615 948 'post_type' => 'office',
616 949 'nopaging' => true,
@@ -624,14 +957,16 @@
624 957 while ($office_query->have_posts())
625 958 {
626 959 $office_query->the_post();
627 960
628 - $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.
629 963 if ( isset( $_GET['_office_id'] ) && ! empty( $_GET['_office_id'] ) )
630 964 {
965 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only admin list display or query; no state change.
631 966 $output .= selected( $post->ID, (int)$_GET['_office_id'], false );
632 967 }
633 - $output .= '>' . get_the_title() . '</option>';
968 + $output .= '>' . esc_html(get_the_title()) . '</option>';
634 969 }
635 970 }
636 971
637 972 wp_reset_postdata();
@@ -641,8 +976,24 @@
641 976 return $output;
642 977 }
643 978
644 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 + /**
645 996 * Show am appraisal filter box
646 997 */
647 998 public function appraisal_filters() {
648 999 global $wp_query;
@@ -649,10 +1000,12 @@
649 1000
650 1001 $output = '';
651 1002
652 1003 $output .= $this->appraisal_status_filter();
653 - $output .= $this->appraisal_attending_negotiator_filter();
1004 + $output .= $this->negotiator_filter();
1005 + $output .= $this->date_range_filter();
654 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.
655 1008 echo apply_filters( 'propertyhive_appraisal_filters', $output );
656 1009 }
657 1010
658 1011 /**
@@ -660,38 +1013,40 @@
660 1013 */
661 1014 public function appraisal_status_filter() {
662 1015 global $wp_query;
663 1016
664 - $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 : '';
665 1020
666 1021 // Status filtering
667 1022 $output = '<select name="_status" id="dropdown_appraisal_status">';
668 1023
669 - $output .= '<option value="">All Statuses</option>';
1024 + $output .= '<option value="">' . esc_html__( 'All Statuses', 'propertyhive' ) . '</option>';
670 1025
671 1026 $output .= '<option value="pending"';
672 1027 $output .= selected( 'pending', $selected_status, false );
673 - $output .= '>' . __( 'Pending', 'propertyhive' ) . '</option>';
1028 + $output .= '>' . esc_html(__( 'Pending', 'propertyhive' )) . '</option>';
674 1029
675 1030 $output .= '<option value="carried_out"';
676 1031 $output .= selected( 'carried_out', $selected_status, false );
677 - $output .= '>' . __( 'Carried Out', 'propertyhive' ) . '</option>';
1032 + $output .= '>' . esc_html(__( 'Carried Out', 'propertyhive' )) . '</option>';
678 1033
679 1034 $output .= '<option value="won"';
680 1035 $output .= selected( 'won', $selected_status, false );
681 - $output .= '>- ' . __( 'Won', 'propertyhive' ) . '</option>';
1036 + $output .= '>- ' . esc_html(__( 'Won', 'propertyhive' )) . '</option>';
682 1037
683 1038 $output .= '<option value="lost"';
684 1039 $output .= selected( 'lost', $selected_status, false );
685 - $output .= '>- ' . __( 'Lost', 'propertyhive' ) . '</option>';
1040 + $output .= '>- ' . esc_html(__( 'Lost', 'propertyhive' )) . '</option>';
686 1041
687 1042 $output .= '<option value="instructed"';
688 1043 $output .= selected( 'instructed', $selected_status, false );
689 - $output .= '>- ' . __( 'Instructed', 'propertyhive' ) . '</option>';
1044 + $output .= '>- ' . esc_html(__( 'Instructed', 'propertyhive' )) . '</option>';
690 1045
691 1046 $output .= '<option value="cancelled"';
692 1047 $output .= selected( 'cancelled', $selected_status, false );
693 - $output .= '>' . __( 'Cancelled', 'propertyhive' ) . '</option>';
1048 + $output .= '>' . esc_html(__( 'Cancelled', 'propertyhive' )) . '</option>';
694 1049
695 1050 $output .= '</select>';
696 1051
697 1052 return $output;
@@ -697,58 +1052,22 @@
697 1052 return $output;
698 1053 }
699 1054
700 1055 /**
701 - * Show an appraisal attending negotiator filter box
702 - */
703 - public function appraisal_attending_negotiator_filter() {
704 - global $wp_query;
705 -
706 - $selected_negotiator_id = isset( $_GET['_negotiator_id']) ? (int)$_GET['_negotiator_id'] : '';
707 -
708 - // Status filtering
709 - $output = '<select name="_negotiator_id" id="dropdown_appraisal_negotiator_id">';
710 -
711 - $output .= '<option value="">Attending Negotiator</option>';
712 - $output .= '<option value="">All Negotiators</option>';
713 -
714 - $args = array(
715 - 'number' => 9999,
716 - 'orderby' => 'display_name',
717 - 'role__not_in' => array('property_hive_contact')
718 - );
719 - $user_query = new WP_User_Query( $args );
720 -
721 - if ( ! empty( $user_query->results ) )
722 - {
723 - foreach ( $user_query->results as $user )
724 - {
725 - $output .= '<option value="' . $user->ID . '"';
726 - if ( $user->ID == $selected_negotiator_id )
727 - {
728 - $output .= ' selected';
729 - }
730 - $output .= '>' . $user->display_name . '</option>';
731 - }
732 - }
733 -
734 - $output .= '</select>';
735 -
736 - return $output;
737 - }
738 -
739 - /**
740 1056 * Show a viewing filter box
741 1057 */
742 1058 public function viewing_filters() {
743 1059 global $wp_query;
744 -
1060 +
745 1061 // Department filtering
746 1062 $output = '';
747 -
1063 +
748 1064 $output .= $this->viewing_status_filter();
749 - $output .= $this->viewing_attending_negotiator_filter();
1065 + $output .= $this->property_office_filter();
1066 + $output .= $this->negotiator_filter();
1067 + $output .= $this->date_range_filter();
750 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.
751 1070 echo apply_filters( 'propertyhive_viewing_filters', $output );
752 1071 }
753 1072
754 1073 /**
@@ -756,85 +1075,56 @@
756 1075 */
757 1076 public function viewing_status_filter() {
758 1077 global $wp_query;
759 1078
760 - $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 : '';
761 1082
762 1083 // Status filtering
763 1084 $output = '<select name="_status" id="dropdown_viewing_status">';
764 -
765 - $output .= '<option value="">All Statuses</option>';
766 1085
767 - $output .= '<option value="pending"';
768 - $output .= selected( 'pending', $selected_status, false );
769 - $output .= '>' . __( 'Pending', 'propertyhive' ) . '</option>';
1086 + $output .= '<option value="">' . esc_html__( 'All Statuses', 'propertyhive' ) . '</option>';
770 1087
771 - $output .= '<option value="confirmed"';
772 - $output .= selected( 'confirmed', $selected_status, false );
773 - $output .= '>- ' . __( 'Confirmed', 'propertyhive' ) . '</option>';
1088 + $viewing_statuses = ph_get_viewing_statuses();
774 1089
775 - $output .= '<option value="unconfirmed"';
776 - $output .= selected( 'unconfirmed', $selected_status, false );
777 - $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 + }
778 1096
779 - $output .= '<option value="carried_out"';
780 - $output .= selected( 'carried_out', $selected_status, false );
781 - $output .= '>' . __( 'Carried Out', 'propertyhive' ) . '</option>';
1097 + $output .= '</select>';
782 1098
783 - $output .= '<option value="feedback_passed_on"';
784 - $output .= selected( 'feedback_passed_on', $selected_status, false );
785 - $output .= '>- ' . __( 'Feedback Passed On', 'propertyhive' ) . '</option>';
1099 + return $output;
1100 + }
786 1101
787 - $output .= '<option value="feedback_not_passed_on"';
788 - $output .= selected( 'feedback_not_passed_on', $selected_status, false );
789 - $output .= '>- ' . __( 'Feedback Not Passed On', 'propertyhive' ) . '</option>';
790 1102
791 - $output .= '<option value="cancelled"';
792 - $output .= selected( 'cancelled', $selected_status, false );
793 - $output .= '>' . __( 'Cancelled', 'propertyhive' ) . '</option>';
794 -
795 - $output .= '</select>';
1103 + public function refresh_property_office_filtering( $query ) {
1104 + remove_filter('posts_join', array( $this, 'filter_by_property_office') );
796 1105
797 - 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 + };
798 1114 }
799 1115
800 - /**
801 - * Show a viewing attending negotiator filter box
802 - */
803 - public function viewing_attending_negotiator_filter() {
804 - global $wp_query;
805 1116
806 - $selected_negotiator_id = isset( $_GET['_negotiator_id']) ? (int)$_GET['_negotiator_id'] : '';
807 -
808 - // Status filtering
809 - $output = '<select name="_negotiator_id" id="dropdown_viewing_negotiator_id">';
810 -
811 - $output .= '<option value="">Attending Negotiator</option>';
812 - $output .= '<option value="">All Negotiators</option>';
1117 + public function filter_by_property_office($query) {
1118 + global $wpdb;
813 1119
814 - $args = array(
815 - 'number' => 9999,
816 - 'orderby' => 'display_name',
817 - 'role__not_in' => array('property_hive_contact')
818 - );
819 - $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;
820 1122
821 - if ( ! empty( $user_query->results ) )
822 - {
823 - foreach ( $user_query->results as $user )
824 - {
825 - $output .= '<option value="' . $user->ID . '"';
826 - if ( $user->ID == $selected_negotiator_id )
827 - {
828 - $output .= ' selected';
829 - }
830 - $output .= '>' . $user->display_name . '</option>';
831 - }
832 - }
833 -
834 - $output .= '</select>';
835 -
836 - 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;
837 1127 }
838 1128
839 1129 /**
840 1130 * Show an offer filter box
@@ -844,9 +1134,12 @@
844 1134
845 1135 $output = '';
846 1136
847 1137 $output .= $this->offer_status_filter();
1138 + $output .= $this->property_office_filter();
1139 + $output .= $this->date_range_filter();
848 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.
849 1142 echo apply_filters( 'propertyhive_offer_filters', $output );
850 1143 }
851 1144
852 1145 /**
@@ -854,27 +1147,26 @@
854 1147 */
855 1148 public function offer_status_filter() {
856 1149 global $wp_query;
857 1150
858 - $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 : '';
859 1154
860 1155 // Status filtering
861 1156 $output = '<select name="_status" id="dropdown_offer_status">';
862 -
863 - $output .= '<option value="">All Statuses</option>';
864 1157
865 - $output .= '<option value="pending"';
866 - $output .= selected( 'pending', $selected_status, false );
867 - $output .= '>' . __( 'Pending', 'propertyhive' ) . '</option>';
1158 + $output .= '<option value="">' . esc_html(__( 'All Statuses', 'propertyhive' )) . '</option>';
868 1159
869 - $output .= '<option value="accepted"';
870 - $output .= selected( 'accepted', $selected_status, false );
871 - $output .= '>' . __( 'Accepted', 'propertyhive' ) . '</option>';
1160 + $offer_statuses = ph_get_offer_statuses();
872 1161
873 - $output .= '<option value="declined"';
874 - $output .= selected( 'declined', $selected_status, false );
875 - $output .= '>' . __( 'Declined', 'propertyhive' ) . '</option>';
876 -
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 +
877 1169 $output .= '</select>';
878 1170
879 1171 return $output;
880 1172 }
@@ -887,9 +1179,12 @@
887 1179
888 1180 $output = '';
889 1181
890 1182 $output .= $this->sale_status_filter();
1183 + $output .= $this->property_office_filter();
1184 + $output .= $this->date_range_filter();
891 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.
892 1187 echo apply_filters( 'propertyhive_sale_filters', $output );
893 1188 }
894 1189
895 1190 /**
@@ -897,35 +1192,193 @@
897 1192 */
898 1193 public function sale_status_filter() {
899 1194 global $wp_query;
900 1195
901 - $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 : '';
902 1199
903 1200 // Status filtering
904 1201 $output = '<select name="_status" id="dropdown_sale_status">';
905 1202
906 - $output .= '<option value="">All Statuses</option>';
1203 + $output .= '<option value="">' . esc_html__( 'All Statuses', 'propertyhive' ) . '</option>';
907 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 +
908 1253 $output .= '<option value="current"';
909 1254 $output .= selected( 'current', $selected_status, false );
910 - $output .= '>' . __( 'Current', 'propertyhive' ) . '</option>';
1255 + $output .= '> ' . esc_html(__( 'Current', 'propertyhive' )) . '</option>';
911 1256
912 - $output .= '<option value="exchanged"';
913 - $output .= selected( 'exchanged', $selected_status, false );
914 - $output .= '>' . __( 'Exchanged', 'propertyhive' ) . '</option>';
1257 + $output .= '<option value="finished"';
1258 + $output .= selected( 'finished', $selected_status, false );
1259 + $output .= '> ' . esc_html(__( 'Finished', 'propertyhive' )) . '</option>';
915 1260
916 - $output .= '<option value="completed"';
917 - $output .= selected( 'completed', $selected_status, false );
918 - $output .= '>' . __( 'Completed', 'propertyhive' ) . '</option>';
1261 + $output .= '</select>';
919 1262
920 - $output .= '<option value="fallen_through"';
921 - $output .= selected( 'fallen_through', $selected_status, false );
922 - $output .= '>' . __( 'Fallen Through', 'propertyhive' ) . '</option>';
923 -
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 +
924 1293 $output .= '</select>';
925 1294
926 1295 return $output;
927 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 + }
928 1381
929 1382 /**
930 1383 * Filters and sorting handler
931 1384 * @param array $vars
@@ -933,50 +1386,71 @@
933 1386 */
934 1387 public function request_query( $vars ) {
935 1388 global $typenow, $wp_query;
936 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.
937 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.
938 1393 if ( !isset($vars['tax_query']) ) { $vars['tax_query'] = array(); }
939 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 +
940 1403 if ( 'property' === $typenow )
941 1404 {
942 - 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 ) ) {
943 1407 $vars['meta_query'][] = array(
944 1408 'key' => '_department',
945 - 'value' => sanitize_text_field( $_GET['_department'] ),
1409 + 'value' => $department,
946 1410 );
947 1411 }
1412 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only admin list display or query; no state change.
948 1413 if ( ! empty( $_GET['_office_id'] ) ) {
949 1414 $vars['meta_query'][] = array(
950 1415 'key' => '_office_id',
1416 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only admin list display or query; no state change.
951 1417 'value' => (int)$_GET['_office_id'],
952 1418 );
953 1419 }
1420 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only admin list display or query; no state change.
954 1421 if ( ! empty( $_GET['_negotiator_id'] ) ) {
955 1422 $vars['meta_query'][] = array(
956 1423 'key' => '_negotiator_id',
1424 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only admin list display or query; no state change.
957 1425 'value' => (int)$_GET['_negotiator_id'],
958 1426 );
959 1427 }
1428 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only admin list display or query; no state change.
960 1429 if ( ! empty( $_GET['_location_id'] ) ) {
961 1430 $vars['tax_query'][] = array(
962 1431 'taxonomy' => 'location',
1432 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only admin list display or query; no state change.
963 1433 'terms' => ( (is_array($_GET['_location_id'])) ? (int)$_GET['_location_id'] : array( (int)$_GET['_location_id'] ) )
964 1434 );
965 1435 }
1436 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only admin list display or query; no state change.
966 1437 if ( ! empty( $_GET['_availability_id'] ) ) {
967 1438 $vars['tax_query'][] = array(
968 1439 'taxonomy' => 'availability',
1440 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only admin list display or query; no state change.
969 1441 'terms' => ( (is_array($_GET['_availability_id'])) ? (int)$_GET['_availability_id'] : array( (int)$_GET['_availability_id'] ) )
970 1442 );
971 1443 }
972 - 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 ) {
973 1446 $vars['meta_query'][] = array(
974 1447 'key' => '_on_market',
975 1448 'value' => 'yes',
976 1449 );
977 1450 }
978 - 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 ) {
979 1453 $vars['meta_query'][] = array(
980 1454 'key' => '_on_market',
981 1455 'value' => 'yes',
982 1456 'compare' => '!=',
@@ -981,16 +1455,18 @@
981 1455 'value' => 'yes',
982 1456 'compare' => '!=',
983 1457 );
984 1458 }
985 - 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 ) {
986 1461 $vars['meta_query'][] = array(
987 1462 'key' => '_featured',
988 1463 'value' => 'yes',
989 1464 );
990 - }
991 - if ( ! empty( $_GET['_marketing'] ) && substr($_GET['_marketing'], 0, 15) == 'marketing_flag_' ) {
992 - $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 );
993 1469 $vars['tax_query'][] = array(
994 1470 'taxonomy' => 'marketing_flag',
995 1471 'terms' => ( (is_array($marketing_flag_id)) ? $marketing_flag_id : array( $marketing_flag_id ) )
996 1472 );
@@ -997,11 +1473,11 @@
997 1473 }
998 1474 }
999 1475 elseif ( 'contact' === $typenow )
1000 1476 {
1001 - 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 ) )
1002 1479 {
1003 - $contact_type = ph_clean($_GET['_contact_type']);
1004 1480 if ( $contact_type == 'hotapplicant' )
1005 1481 {
1006 1482 $contact_type = 'applicant';
1007 1483
@@ -1015,34 +1491,63 @@
1015 1491 'value' => $contact_type,
1016 1492 'compare' => 'LIKE'
1017 1493 );
1018 1494 }
1495 +
1496 + $vars = $this->filter_by_date_range($vars, 'date_query');
1019 1497 }
1020 - elseif ( 'enquiry' === $typenow )
1498 + elseif ( 'enquiry' === $typenow )
1021 1499 {
1022 - 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 +
1023 1503 $vars['meta_query'][] = array(
1024 1504 'key' => '_status',
1025 - 'value' => sanitize_text_field( $_GET['_status'] ),
1505 + 'value' => $status,
1026 1506 );
1027 1507 }
1028 - 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 ) ) {
1029 1521 $vars['meta_query'][] = array(
1030 1522 'key' => '_source',
1031 - 'value' => sanitize_text_field( $_GET['_source'] ),
1523 + 'value' => $source,
1032 1524 );
1033 1525 }
1526 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only admin list display or query; no state change.
1034 1527 if ( ! empty( $_GET['_office_id'] ) ) {
1035 1528 $vars['meta_query'][] = array(
1036 1529 'key' => '_office_id',
1037 - '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'],
1038 1532 );
1039 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');
1040 1544 }
1041 - elseif ( 'appraisal' === $typenow )
1545 + elseif ( 'appraisal' === $typenow )
1042 1546 {
1043 - if ( ! empty( $_GET['_status'] ) ) {
1044 - 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 )
1045 1550 {
1046 1551 case "confirmed":
1047 1552 {
1048 1553 $vars['meta_query'][] = array(
@@ -1070,123 +1575,449 @@
1070 1575 default:
1071 1576 {
1072 1577 $vars['meta_query'][] = array(
1073 1578 'key' => '_status',
1074 - 'value' => sanitize_text_field( $_GET['_status'] ),
1579 + 'value' => $status,
1075 1580 );
1076 1581 }
1077 1582 }
1078 1583 }
1584 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only admin list display or query; no state change.
1079 1585 if ( ! empty( $_GET['_negotiator_id'] ) )
1080 1586 {
1081 1587 $vars['meta_query'][] = array(
1082 1588 'key' => '_negotiator_id',
1589 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only admin list display or query; no state change.
1083 1590 'value' => (int)$_GET['_negotiator_id'],
1084 1591 );
1085 1592 }
1593 +
1594 + $vars = $this->filter_by_date_range($vars);
1086 1595 }
1087 1596 elseif ( 'viewing' === $typenow )
1088 1597 {
1089 - if ( ! empty( $_GET['_status'] ) ) {
1090 - 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 )
1091 1648 {
1092 - case "confirmed":
1093 - {
1649 + case 'pending' :
1094 1650 $vars['meta_query'][] = array(
1095 - 'key' => '_status',
1096 - 'value' => 'pending',
1651 + 'key' => '_start_date',
1652 + 'value' => gmdate('Y-m-d'),
1653 + 'type' => 'date',
1654 + 'compare' => '>',
1097 1655 );
1656 + break;
1657 +
1658 + case 'current' :
1098 1659 $vars['meta_query'][] = array(
1099 - 'key' => '_all_confirmed',
1100 - '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 + )
1101 1688 );
1102 1689 break;
1103 - }
1104 - case "unconfirmed":
1105 - {
1690 +
1691 + case 'finished':
1106 1692 $vars['meta_query'][] = array(
1107 - 'key' => '_status',
1108 - 'value' => 'pending',
1693 + 'key' => '_end_date',
1694 + 'value' => gmdate('Y-m-d'),
1695 + 'type' => 'date',
1696 + 'compare' => '<',
1109 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':
1110 1722 $vars['meta_query'][] = array(
1111 - 'key' => '_all_confirmed',
1112 - 'value' => '',
1723 + 'key' => '_key_date_status',
1724 + 'value' => $value,
1113 1725 );
1114 1726 break;
1115 - }
1116 - case "feedback_passed_on":
1117 - {
1727 + case 'pending':
1118 1728 $vars['meta_query'][] = array(
1119 - 'key' => '_status',
1120 - 'value' => 'carried_out',
1729 + 'key' => '_key_date_status',
1730 + 'value' => 'pending',
1121 1731 );
1732 + break;
1733 + case 'overdue':
1122 1734 $vars['meta_query'][] = array(
1123 - 'key' => '_feedback_status',
1124 - 'value' => array('interested', 'not_interested'),
1735 + 'key' => '_key_date_status',
1736 + 'value' => array('pending', 'booked'),
1125 1737 'compare' => 'IN'
1126 1738 );
1127 1739 $vars['meta_query'][] = array(
1128 - 'key' => '_feedback_passed_on',
1129 - 'value' => 'yes',
1740 + 'key' => '_date_due',
1741 + 'value' => gmdate("Y-m-d"),
1742 + 'type' => 'date',
1743 + 'compare' => '<',
1130 1744 );
1131 1745 break;
1132 - }
1133 - case "feedback_not_passed_on":
1134 - {
1135 - $vars['meta_query'][] = array(
1136 - 'key' => '_status',
1137 - 'value' => 'carried_out',
1138 - );
1746 + case 'upcoming_and_overdue':
1139 1747 $vars['meta_query'][] = array(
1140 - 'key' => '_feedback_status',
1141 - 'value' => array('interested', 'not_interested'),
1748 + 'key' => '_key_date_status',
1749 + 'value' => array('pending', 'booked'),
1142 1750 'compare' => 'IN'
1143 1751 );
1752 + $upcoming_threshold = new DateTime('+ ' . apply_filters( 'propertyhive_key_date_upcoming_days', 7 ) . ' DAYS');
1144 1753 $vars['meta_query'][] = array(
1145 - 'key' => '_feedback_passed_on',
1146 - 'value' => '',
1754 + 'key' => '_date_due',
1755 + 'value' => $upcoming_threshold->format('Y-m-d'),
1756 + 'type' => 'date',
1757 + 'compare' => '<=',
1147 1758 );
1148 1759 break;
1149 - }
1150 - default:
1151 - {
1152 - $vars['meta_query'][] = array(
1153 - 'key' => '_status',
1154 - 'value' => sanitize_text_field( $_GET['_status'] ),
1155 - );
1156 - }
1157 1760 }
1158 1761 }
1159 - 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'] ) )
1160 1765 {
1161 1766 $vars['meta_query'][] = array(
1162 - 'key' => '_negotiator_id',
1163 - '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'],
1164 1770 );
1165 1771 }
1772 +
1773 + $vars = $this->filter_by_date_range($vars, '_date_due');
1166 1774 }
1167 - elseif ( 'offer' === $typenow )
1168 - {
1169 - if ( ! empty( $_GET['_status'] ) ) {
1170 - $vars['meta_query'][] = array(
1171 - 'key' => '_status',
1172 - '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',
1173 1801 );
1174 1802 }
1803 + else
1804 + {
1805 + // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- Add validated date boundaries using the fixed date key selected for this paginated admin post-type list.
1806 + $vars['meta_query'] = array_merge($vars['meta_query'], array (
1807 + array(
1808 + 'key' => $meta_key,
1809 + 'value' => $date_range_from,
1810 + 'type' => 'date',
1811 + 'compare' => '>='
1812 + ),
1813 + array(
1814 + 'key' => $meta_key,
1815 + 'value' => $date_range_to,
1816 + 'type' => 'date',
1817 + 'compare' => '<='
1818 + ),
1819 + ));
1820 + }
1821 + }
1822 +
1823 + return $vars;
1824 + }
1825 +
1826 + public function posts_join( $join, $q ) {
1827 + global $typenow, $wp_query, $wpdb;
1828 +
1829 + if ( !$q->is_main_query() )
1830 + return $join;
1831 +
1832 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only admin list display or query; no state change.
1833 + $search = isset( $_GET['s'] ) && is_string( $_GET['s'] ) ? sanitize_text_field( wp_unslash( $_GET['s'] ) ) : '';
1834 + if ( $search === '' ) {
1835 + return $join;
1175 1836 }
1176 - elseif ( 'sale' === $typenow )
1837 +
1838 + if ( 'property' === $typenow )
1177 1839 {
1178 - if ( ! empty( $_GET['_status'] ) ) {
1179 - $vars['meta_query'][] = array(
1180 - 'key' => '_status',
1181 - 'value' => sanitize_text_field( $_GET['_status'] ),
1182 - );
1840 + $join .= "
1841 +LEFT JOIN " . $wpdb->postmeta . " AS ph_property_filter_meta_address_concatenated ON " . $wpdb->posts . ".ID = ph_property_filter_meta_address_concatenated.post_id AND ph_property_filter_meta_address_concatenated.meta_key = '_address_concatenated'
1842 +LEFT JOIN " . $wpdb->postmeta . " AS ph_property_filter_meta_reference_number ON " . $wpdb->posts . ".ID = ph_property_filter_meta_reference_number.post_id AND ph_property_filter_meta_reference_number.meta_key = '_reference_number'
1843 +LEFT JOIN " . $wpdb->postmeta . " AS ph_property_filter_meta_owner_details ON " . $wpdb->posts . ".ID = ph_property_filter_meta_owner_details.post_id AND ph_property_filter_meta_owner_details.meta_key = '_owner_details'
1844 +";
1845 + }
1846 + elseif ( 'contact' === $typenow )
1847 + {
1848 + $phone_number = '';
1849 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only admin list display or query; no state change.
1850 + if ( is_numeric(substr($search, 0, 1)) )
1851 + {
1852 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only admin list display or query; no state change.
1853 + $phone_number = preg_replace( "/[^0-9,]/", "", $search );
1183 1854 }
1855 +
1856 + $join .= "
1857 +LEFT JOIN " . $wpdb->postmeta . " AS ph_contact_filter_meta_address_concatenated ON " . $wpdb->posts . ".ID = ph_contact_filter_meta_address_concatenated.post_id AND ph_contact_filter_meta_address_concatenated.meta_key = '_address_concatenated'
1858 +LEFT JOIN " . $wpdb->postmeta . " AS ph_contact_filter_meta_email_address ON " . $wpdb->posts . ".ID = ph_contact_filter_meta_email_address.post_id AND ph_contact_filter_meta_email_address.meta_key = '_email_address' ";
1859 +
1860 + if ( $phone_number != '' )
1861 + {
1862 + $join .= " LEFT JOIN " . $wpdb->postmeta . " AS ph_contact_filter_meta_telephone_number ON " . $wpdb->posts . ".ID = ph_contact_filter_meta_telephone_number.post_id AND ph_contact_filter_meta_telephone_number.meta_key = '_telephone_number_clean'
1863 + ";
1864 + }
1184 1865 }
1866 + elseif ( 'appraisal' === $typenow )
1867 + {
1868 + $join .= "
1869 +LEFT JOIN " . $wpdb->postmeta . " AS ph_appraisal_filter_meta_name_number ON " . $wpdb->posts . ".ID = ph_appraisal_filter_meta_name_number.post_id AND ph_appraisal_filter_meta_name_number.meta_key = '_address_name_number'
1870 +LEFT JOIN " . $wpdb->postmeta . " AS ph_appraisal_filter_meta_street ON " . $wpdb->posts . ".ID = ph_appraisal_filter_meta_street.post_id AND ph_appraisal_filter_meta_street.meta_key = '_address_street'
1871 +LEFT JOIN " . $wpdb->postmeta . " AS ph_appraisal_filter_meta_2 ON " . $wpdb->posts . ".ID = ph_appraisal_filter_meta_2.post_id AND ph_appraisal_filter_meta_2.meta_key = '_address_two'
1872 +LEFT JOIN " . $wpdb->postmeta . " AS ph_appraisal_filter_meta_3 ON " . $wpdb->posts . ".ID = ph_appraisal_filter_meta_3.post_id AND ph_appraisal_filter_meta_3.meta_key = '_address_three'
1873 +LEFT JOIN " . $wpdb->postmeta . " AS ph_appraisal_filter_meta_4 ON " . $wpdb->posts . ".ID = ph_appraisal_filter_meta_4.post_id AND ph_appraisal_filter_meta_4.meta_key = '_address_four'
1874 +LEFT JOIN " . $wpdb->postmeta . " AS ph_appraisal_filter_meta_postcode ON " . $wpdb->posts . ".ID = ph_appraisal_filter_meta_postcode.post_id AND ph_appraisal_filter_meta_postcode.meta_key = '_address_postcode'
1875 +";
1876 + }
1877 + elseif ( 'viewing' === $typenow || 'offer' === $typenow || 'sale' === $typenow || 'tenancy' === $typenow )
1878 + {
1879 + $join .= "
1880 +LEFT JOIN " . $wpdb->postmeta . " AS ph_property_filter_meta ON " . $wpdb->posts . ".ID = ph_property_filter_meta.post_id AND ph_property_filter_meta.meta_key = '_property_id'
1881 +LEFT JOIN " . $wpdb->posts . " AS ph_property_filter_posts ON ph_property_filter_posts.ID = ph_property_filter_meta.meta_value
1882 +LEFT JOIN " . $wpdb->postmeta . " AS ph_property_filter_meta_address_concatenated ON ph_property_filter_posts.ID = ph_property_filter_meta_address_concatenated.post_id AND ph_property_filter_meta_address_concatenated.meta_key = '_address_concatenated'
1883 +LEFT JOIN " . $wpdb->postmeta . " AS ph_property_filter_meta_reference_number ON ph_property_filter_posts.ID = ph_property_filter_meta_reference_number.post_id AND ph_property_filter_meta_reference_number.meta_key = '_reference_number'
1884 +LEFT JOIN " . $wpdb->postmeta . " AS ph_applicant_filter_meta ON " . $wpdb->posts . ".ID = ph_applicant_filter_meta.post_id AND ph_applicant_filter_meta.meta_key = '_applicant_contact_id'
1885 +LEFT JOIN " . $wpdb->posts . " AS ph_applicant_filter_posts ON ph_applicant_filter_posts.ID = ph_applicant_filter_meta.meta_value
1886 +";
1887 + }
1185 1888
1186 - $vars = apply_filters( 'propertyhive_property_filter_query', $vars, $typenow );
1889 + return $join;
1890 + }
1187 1891
1188 - return $vars;
1892 + public function posts_where( $where, $q ) {
1893 + global $typenow, $wp_query, $wpdb;
1894 +
1895 + if ( !$q->is_main_query() )
1896 + return $where;
1897 +
1898 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only admin list display or query; no state change.
1899 + $search = isset( $_GET['s'] ) && is_string( $_GET['s'] ) ? sanitize_text_field( wp_unslash( $_GET['s'] ) ) : '';
1900 + if ( $search === '' ) {
1901 + return $where;
1902 + }
1903 + $reference_like = $wpdb->prepare( '%s', $wpdb->esc_like( $search ) . '%' );
1904 + $reference_exact = $wpdb->prepare( '%s', $search );
1905 + $phone_number = '';
1906 +
1907 + if ( 'property' === $typenow )
1908 + {
1909 + $where = preg_replace_callback(
1910 + "/\(\s*" . $wpdb->posts . ".post_title\s+LIKE\s*('(?:\\\\.|[^'\\\\])*')\s*\)/",
1911 + static function( $matches ) use ( $wpdb, $reference_like, $reference_exact, $phone_number ) {
1912 + return "(
1913 + (" . $wpdb->posts . ".post_title LIKE " . $matches[1] . ")
1914 + OR
1915 + (ph_property_filter_meta_address_concatenated.meta_value LIKE " . $matches[1] . ")
1916 + OR
1917 + (ph_property_filter_meta_reference_number.meta_value LIKE " . $reference_like . ")
1918 + OR
1919 + (ph_property_filter_meta_owner_details.meta_value LIKE " . $matches[1] . ")
1920 + )";
1921 + },
1922 + $where
1923 + );
1924 +
1925 + $where = preg_replace(
1926 + "/\s+OR\s+\(\s*" . $wpdb->posts . ".post_excerpt\s+LIKE\s*('(?:\\\\.|[^'\\\\])*')\s*\)/",
1927 + "",
1928 + $where
1929 + );
1930 +
1931 + $where = preg_replace(
1932 + "/\s+OR\s+\(\s*" . $wpdb->posts . ".post_content\s+LIKE\s*('(?:\\\\.|[^'\\\\])*')\s*\)/",
1933 + "",
1934 + $where
1935 + );
1936 + }
1937 + elseif ( 'contact' === $typenow )
1938 + {
1939 + $phone_number = '';
1940 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only admin list display or query; no state change.
1941 + if ( is_numeric(substr($search, 0, 1)) )
1942 + {
1943 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only admin list display or query; no state change.
1944 + $phone_number = preg_replace( "/[^0-9,]/", "", $search );
1945 + }
1946 +
1947 + $where = preg_replace_callback(
1948 + "/\(\s*" . $wpdb->posts . ".post_title\s+LIKE\s*('(?:\\\\.|[^'\\\\])*')\s*\)/",
1949 + static function( $matches ) use ( $wpdb, $reference_like, $reference_exact, $phone_number ) {
1950 + return "(
1951 + (" . $wpdb->posts . ".post_title LIKE " . $matches[1] . ")
1952 + OR
1953 + (ph_contact_filter_meta_address_concatenated.meta_value LIKE " . $matches[1] . ")
1954 + OR
1955 + (ph_contact_filter_meta_email_address.meta_value LIKE " . $matches[1] . ")
1956 + " . ( $phone_number != '' ? "OR (ph_contact_filter_meta_telephone_number.meta_value LIKE '%" . $phone_number . "%')" : '' ) . "
1957 + )";
1958 + },
1959 + $where
1960 + );
1961 +
1962 + $where = preg_replace(
1963 + "/\s+OR\s+\(\s*" . $wpdb->posts . ".post_excerpt\s+LIKE\s*('(?:\\\\.|[^'\\\\])*')\s*\)/",
1964 + "",
1965 + $where
1966 + );
1967 +
1968 + $where = preg_replace(
1969 + "/\s+OR\s+\(\s*" . $wpdb->posts . ".post_content\s+LIKE\s*('(?:\\\\.|[^'\\\\])*')\s*\)/",
1970 + "",
1971 + $where
1972 + );
1973 + }
1974 + elseif ( 'appraisal' === $typenow )
1975 + {
1976 + $where = preg_replace_callback(
1977 + "/\(\s*" . $wpdb->posts . ".post_title\s+LIKE\s*('(?:\\\\.|[^'\\\\])*')\s*\)/",
1978 + static function( $matches ) use ( $wpdb, $reference_like, $reference_exact, $phone_number ) {
1979 + return "(
1980 + (" . $wpdb->posts . ".post_title LIKE " . $matches[1] . ")
1981 + OR
1982 + (ph_appraisal_filter_meta_name_number.meta_value LIKE " . $matches[1] . ")
1983 + OR
1984 + (ph_appraisal_filter_meta_street.meta_value LIKE " . $matches[1] . ")
1985 + OR
1986 + (ph_appraisal_filter_meta_2.meta_value LIKE " . $matches[1] . ")
1987 + OR
1988 + (ph_appraisal_filter_meta_3.meta_value LIKE " . $matches[1] . ")
1989 + OR
1990 + (ph_appraisal_filter_meta_4.meta_value LIKE " . $matches[1] . ")
1991 + OR
1992 + (ph_appraisal_filter_meta_postcode.meta_value LIKE " . $matches[1] . ")
1993 + )";
1994 + },
1995 + $where
1996 + );
1997 + }
1998 + elseif ( 'viewing' === $typenow || 'offer' === $typenow || 'sale' === $typenow || 'tenancy' === $typenow )
1999 + {
2000 + $where = preg_replace_callback(
2001 + "/\(\s*" . $wpdb->posts . ".post_title\s+LIKE\s*('(?:\\\\.|[^'\\\\])*')\s*\)/",
2002 + static function( $matches ) use ( $wpdb, $reference_like, $reference_exact, $phone_number ) {
2003 + return "(
2004 + (" . $wpdb->posts . ".post_title LIKE " . $matches[1] . ")
2005 + OR
2006 + (ph_property_filter_posts.post_title LIKE " . $matches[1] . ")
2007 + OR
2008 + (ph_property_filter_meta_address_concatenated.meta_value LIKE " . $matches[1] . ")
2009 + OR
2010 + (ph_property_filter_meta_reference_number.meta_value = " . $reference_exact . ")
2011 + OR
2012 + (ph_applicant_filter_posts.post_title LIKE " . $matches[1] . ")
2013 + )";
2014 + },
2015 + $where
2016 + );
2017 + }
2018 +
2019 + return $where;
1189 2020 }
1190 2021
1191 2022 /**
1192 2023 * Removes variations etc belonging to a deleted post, and clears transients
@@ -1252,5 +2083,5 @@
1252 2083 }
1253 2084
1254 2085 endif;
1255 2086
1256 -return new PH_Admin_Post_Types();
2087 +return new PH_Admin_Post_Types();