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