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