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
propertyhive / includes / admin / class-ph-admin-post-types.php

class-ph-admin-post-types.php in Property Hive 2.4.0, at includes/admin/class-ph-admin-post-types.php

2,087 lines 87.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
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
5 /**
6 * Post Types Admin
7 *
8 * @author PropertyHive
9 * @category Admin
10 * @package PropertyHive/Admin
11 * @version 1.0.0
12 */
13
14 if ( ! defined( 'ABSPATH' ) ) exit; // Exit if accessed directly
15
16 if ( ! class_exists( 'PH_Admin_Post_Types' ) ) :
17
18 /**
19 * PH_Admin_Post_Types Class
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.
22 class PH_Admin_Post_Types {
23
24 /**
25 * Constructor
26 */
27 public function __construct() {
28 add_action( 'admin_init', array( $this, 'include_post_type_handlers' ) );
29 add_filter( 'post_updated_messages', array( $this, 'post_updated_messages' ) );
30 add_action( 'pre_get_posts', array( $this, 'refresh_property_office_filtering' ));
31 add_action( 'admin_print_scripts', array( $this, 'remove_month_filter' ) );
32 add_action( 'admin_print_scripts', array( $this, 'disable_autosave' ) );
33
34 // Filters
35 add_action( 'restrict_manage_posts', array( $this, 'restrict_manage_posts' ) );
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 );
39
40 // Status transitions
41 add_action( 'delete_post', array( $this, 'delete_post' ) );
42 add_action( 'wp_trash_post', array( $this, 'trash_post' ) );
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;
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;
267
268 $post_id = isset($_GET['post']) ? intval($_GET['post']) : 0;
269 $post_type = get_post_type($post_id);
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
306 /**
307 * Conditonally load classes and functions only needed when viewing a post type.
308 */
309 public function include_post_type_handlers() {
310 include( 'post-types/class-ph-admin-header-stripes.php' );
311 include( 'post-types/class-ph-admin-meta-boxes.php' );
312
313 include( 'post-types/class-ph-admin-cpt-property.php' );
314 include( 'post-types/class-ph-admin-cpt-contact.php' );
315 include( 'post-types/class-ph-admin-cpt-enquiry.php' );
316 include( 'post-types/class-ph-admin-cpt-appraisal.php' );
317 include( 'post-types/class-ph-admin-cpt-viewing.php' );
318 include( 'post-types/class-ph-admin-cpt-offer.php' );
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' );
322 }
323
324 /**
325 * Change messages when a post type is updated.
326 *
327 * @param array $messages
328 * @return array
329 */
330 public function post_updated_messages( $messages ) {
331 global $post, $post_ID;
332
333 $messages['property'] = array(
334 0 => '', // Unused. Messages start at index 1.
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) ) ),
337 2 => __( 'Custom field updated.', 'propertyhive' ),
338 3 => __( 'Custom field deleted.', 'propertyhive' ),
339 4 => __( 'Property updated.', 'propertyhive' ),
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) ) ),
343 7 => __( 'Property saved.', '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' ),
348 date_i18n( __( 'M j, Y @ G:i', 'propertyhive' ), strtotime( $post->post_date ) ), esc_url( 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) ) ) ),
351 );
352
353 $messages['contact'] = array(
354 0 => '', // Unused. Messages start at index 1.
355 1 => __( 'Contact updated.', 'propertyhive' ),
356 2 => __( 'Custom field updated.', 'propertyhive' ),
357 3 => __( 'Custom field deleted.', 'propertyhive' ),
358 4 => __( 'Contact updated.', 'propertyhive' ),
359 5 => __( 'Revision restored.', 'propertyhive' ),
360 6 => __( 'Contact published.', 'propertyhive' ),
361 7 => __( 'Contact saved.', 'propertyhive' ),
362 8 => __( 'Contact submitted.', 'propertyhive' ),
363 /* translators: 1: formatted date */
364 9 => sprintf( __( 'Contact scheduled for: <strong>%1$s</strong>.', 'propertyhive' ), date_i18n( __( 'M j, Y @ G:i', 'propertyhive' ), strtotime( $post->post_date ) )),
365 10 => __( 'Contact draft updated.', 'propertyhive' ),
366 );
367
368 $messages['office'] = array(
369 0 => '', // Unused. Messages start at index 1.
370 1 => __( 'Office updated.', 'propertyhive' ),
371 2 => __( 'Custom field updated.', 'propertyhive' ),
372 3 => __( 'Custom field deleted.', 'propertyhive' ),
373 4 => __( 'Office updated.', 'propertyhive' ),
374 5 => __( 'Revision restored.', 'propertyhive' ),
375 6 => sprintf( __( 'Office published.', 'propertyhive' ), esc_url( get_permalink($post_ID) ) ),
376 7 => __( 'Office saved.', 'propertyhive' ),
377 8 => sprintf( __( 'Office submitted.', 'propertyhive' ), esc_url( add_query_arg( 'preview', 'true', get_permalink($post_ID) ) ) ),
378 /* translators: 1: formatted date */
379 9 => sprintf( __( 'Office scheduled for: <strong>%1$s</strong>.', 'propertyhive' ),
380 date_i18n( __( 'M j, Y @ G:i', 'propertyhive' ), strtotime( $post->post_date ) ), esc_url( get_permalink($post_ID) ) ),
381 10 => sprintf( __( 'Office draft updated. ', 'propertyhive' ), esc_url( add_query_arg( 'preview', 'true', get_permalink($post_ID) ) ) ),
382 );
383
384 $messages['enquiry'] = array(
385 0 => '', // Unused. Messages start at index 1.
386 1 => sprintf( __( 'Enquiry updated.', 'propertyhive' ), esc_url( get_permalink($post_ID) ) ),
387 2 => __( 'Custom field updated.', 'propertyhive' ),
388 3 => __( 'Custom field deleted.', 'propertyhive' ),
389 4 => __( 'Enquiry updated.', 'propertyhive' ),
390 5 => __( 'Revision restored.', 'propertyhive' ),
391 6 => sprintf( __( 'Enquiry published.', 'propertyhive' ), esc_url( get_permalink($post_ID) ) ),
392 7 => __( 'Enquiry saved.', 'propertyhive' ),
393 8 => sprintf( __( 'Enquiry submitted.', 'propertyhive' ), esc_url( add_query_arg( 'preview', 'true', get_permalink($post_ID) ) ) ),
394 /* translators: 1: formatted date */
395 9 => sprintf( __( 'Enquiry scheduled for: <strong>%1$s</strong>.', 'propertyhive' ),
396 date_i18n( __( 'M j, Y @ G:i', 'propertyhive' ), strtotime( $post->post_date ) ), esc_url( get_permalink($post_ID) ) ),
397 10 => sprintf( __( 'Enquiry draft updated.', 'propertyhive' ), esc_url( add_query_arg( 'preview', 'true', get_permalink($post_ID) ) ) ),
398 );
399
400 return $messages;
401 }
402
403 /**
404 * Remove month filter from some property hive pages
405 */
406 public function remove_month_filter() {
407 global $typenow;
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) )
413 {
414 add_filter('months_dropdown_results', '__return_empty_array');
415 }
416 }
417
418 /**
419 * Disable the auto-save functionality for certain CPT's.
420 *
421 * @access public
422 * @return void
423 */
424 public function disable_autosave(){
425 /*global $post;
426
427 if ( $post && get_post_type( $post->ID ) === 'enquiry' ) {
428 wp_dequeue_script( 'autosave' );
429 }*/
430 }
431
432 /**
433 * Filters for post types
434 */
435 public function restrict_manage_posts() {
436 global $typenow, $wp_query;
437
438 switch ( $typenow ) {
439 case 'property' :
440 $this->property_filters();
441 break;
442 case 'contact' :
443 $this->contact_filters();
444 break;
445 case 'enquiry' :
446 $this->enquiry_filters();
447 break;
448 case 'appraisal' :
449 $this->appraisal_filters();
450 break;
451 case 'viewing' :
452 $this->viewing_filters();
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;
466 default :
467 break;
468 }
469 }
470
471 /**
472 * Show a property filter box
473 */
474 public function property_filters() {
475 global $wp_query;
476
477 // Department filtering
478 $output = '';
479
480 $output .= $this->property_department_filter();
481 $output .= $this->property_marketing_filter();
482 $output .= $this->property_availability_filter();
483 $output .= $this->property_location_filter();
484 $output .= $this->property_office_filter();
485 $output .= $this->negotiator_filter();
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.
488 echo apply_filters( 'propertyhive_property_filters', $output );
489 }
490
491 /**
492 * Show a property department filter box
493 */
494 public function property_department_filter() {
495 global $wp_query;
496
497 $departments = ph_get_departments();
498
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 : '';
502
503 // Department filtering
504 $output = '<select name="_department" id="dropdown_property_department">';
505
506 $output .= '<option value="">' . esc_html__( 'All Departments', 'propertyhive' ) . '</option>';
507
508 foreach ( $departments as $key => $value )
509 {
510 if ( get_option( 'propertyhive_active_departments_' . str_replace("residential-", "", $key) ) == 'yes' )
511 {
512 $output .= '<option value="' . esc_attr($key) . '"';
513 $output .= selected( $key, $selected_department, false );
514 $output .= '>' . esc_html($value) . '</option>';
515 }
516 }
517
518 $output .= '</select>';
519
520 return $output;
521 }
522
523 /**
524 * Show a property office filter box
525 */
526 public function property_office_filter() {
527 global $wp_query, $post;
528
529 // Department filtering
530 $output = '<select name="_office_id" id="dropdown_property_office_id">';
531
532 $output .= '<option value="">' . esc_html__( 'All Offices', 'propertyhive' ) . '</option>';
533
534 $args = array(
535 'post_type' => 'office',
536 'nopaging' => true,
537 'orderby' => 'title',
538 'order' => 'ASC'
539 );
540 $office_query = new WP_Query($args);
541
542 if ($office_query->have_posts())
543 {
544 while ($office_query->have_posts())
545 {
546 $office_query->the_post();
547
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.
550 if ( isset( $_GET['_office_id'] ) && ! empty( $_GET['_office_id'] ) )
551 {
552 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only admin list display or query; no state change.
553 $output .= selected( $post->ID, (int)$_GET['_office_id'], false );
554 }
555 $output .= '>' . esc_html(get_the_title()) . '</option>';
556 }
557 }
558
559 wp_reset_postdata();
560
561 $output .= '</select>';
562
563 return $output;
564 }
565
566 /**
567 * Show a negotiator filter box
568 */
569 public function negotiator_filter() {
570
571 return wp_dropdown_users(array(
572 'name' => '_negotiator_id',
573 'id' => 'dropdown_property_negotiator_id',
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'],
577 'echo' => false,
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 }
582
583 /**
584 * Show a date range selector
585 */
586 public function date_range_filter() {
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
610 /**
611 * Show a property location filter box
612 */
613 public function property_location_filter() {
614 global $wp_query, $post;
615
616 // Department filtering
617 $output = '<select name="_location_id" id="dropdown_property_location_id">';
618
619 $options = array( );
620 $args = array(
621 'hide_empty' => false,
622 'parent' => 0
623 );
624 $terms = get_terms( array_merge( wp_parse_args( $args ), array( 'taxonomy' => 'location' ) ) );
625
626 if ( !empty( $terms ) && !is_wp_error( $terms ) )
627 {
628 foreach ($terms as $term)
629 {
630 $options[$term->term_id] = $term->name;
631
632 $args = array(
633 'hide_empty' => false,
634 'parent' => $term->term_id
635 );
636 $subterms = get_terms( array_merge( wp_parse_args( $args ), array( 'taxonomy' => 'location' ) ) );
637
638 if ( !empty( $subterms ) && !is_wp_error( $subterms ) )
639 {
640 foreach ($subterms as $term)
641 {
642 $options[$term->term_id] = '- ' . $term->name;
643
644 $args = array(
645 'hide_empty' => false,
646 'parent' => $term->term_id
647 );
648 $subsubterms = get_terms( array_merge( wp_parse_args( $args ), array( 'taxonomy' => 'location' ) ) );
649
650 if ( !empty( $subsubterms ) && !is_wp_error( $subsubterms ) )
651 {
652 foreach ($subsubterms as $term)
653 {
654 $options[$term->term_id] = '- ' . $term->name;
655 }
656 }
657 }
658 }
659 }
660 }
661
662 $output .= '<option value="">' . esc_html(__( 'All Locations', 'propertyhive' )) . '</option>';
663
664 if ( !empty($options) )
665 {
666 foreach ( $options as $value => $label )
667 {
668 $output .= '<option value="' . esc_attr($value) . '"';
669 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only admin list display or query; no state change.
670 if ( isset( $_GET['_location_id'] ) && ! empty( $_GET['_location_id'] ) )
671 {
672 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only admin list display or query; no state change.
673 $output .= selected( $value, (int)$_GET['_location_id'], false );
674 }
675 $output .= '>' . esc_html($label) . '</option>';
676 }
677 }
678
679 $output .= '</select>';
680
681 return $output;
682 }
683
684 /**
685 * Show a property availability filter box
686 */
687 public function property_availability_filter() {
688 global $wp_query, $post;
689
690 // Availability filtering
691 $output = '<select name="_availability_id" id="dropdown_property_availability_id">';
692
693 $options = array( );
694 $args = array(
695 'hide_empty' => false,
696 'parent' => 0
697 );
698 $terms = get_terms( array_merge( wp_parse_args( $args ), array( 'taxonomy' => 'availability' ) ) );
699
700 if ( !empty( $terms ) && !is_wp_error( $terms ) )
701 {
702 foreach ($terms as $term)
703 {
704 $options[$term->term_id] = $term->name;
705 }
706 }
707
708 $output .= '<option value="">' . esc_html(__( 'All Availabilities', 'propertyhive' )) . '</option>';
709
710 if ( !empty($options) )
711 {
712 foreach ( $options as $value => $label )
713 {
714 $output .= '<option value="' . esc_attr($value) . '"';
715 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only admin list display or query; no state change.
716 if ( isset( $_GET['_availability_id'] ) && ! empty( $_GET['_availability_id'] ) )
717 {
718 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only admin list display or query; no state change.
719 $output .= selected( $value, (int)$_GET['_availability_id'], false );
720 }
721 $output .= '>' . esc_html($label) . '</option>';
722 }
723 }
724
725 $output .= '</select>';
726
727 return $output;
728 }
729
730 /**
731 * Show a property marketing filter box
732 */
733 public function property_marketing_filter() {
734 global $wp_query, $post;
735
736 // Availability filtering
737 $output = '<select name="_marketing" id="dropdown_property_marketing">';
738
739 $output .= '<option value="">' . esc_html__( 'All Marketing Statuses', 'propertyhive' ) . '</option>';
740
741 $options = array(
742 'on_market' => __( 'On Market Only', 'propertyhive' ),
743 'off_market' => __( 'Not On Market Only', 'propertyhive' ),
744 'featured' => __( 'Featured Only', 'propertyhive' ),
745 );
746
747 $args = array(
748 'hide_empty' => false,
749 'parent' => 0
750 );
751 $terms = get_terms( array_merge( wp_parse_args( $args ), array( 'taxonomy' => 'marketing_flag' ) ) );
752
753 if ( !empty( $terms ) && !is_wp_error( $terms ) )
754 {
755 foreach ($terms as $term)
756 {
757 $options['marketing_flag_' . $term->term_id] = __( 'Has Marketing Flag', 'propertyhive') . ' - ' . $term->name;
758 }
759 }
760
761 $options = apply_filters( 'propertyhive_property_filter_marketing_options', $options );
762 $selected_marketing = $this->get_admin_query_value( '_marketing' );
763
764 foreach ( $options as $key => $value )
765 {
766 $output .= '<option value="' . esc_attr($key) . '"';
767 if ( ! empty( $selected_marketing ) )
768 {
769 $output .= selected( $key, $selected_marketing, false );
770 }
771 $output .= '>' . esc_html($value) . '</option>';
772 }
773
774 $output .= '</select>';
775
776 return $output;
777 }
778
779 /**
780 * Show a contact filter box
781 */
782 public function contact_filters() {
783 global $wp_query;
784
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 : '';
788
789 // Type filtering
790 $options = array();
791
792 // Owners
793 $option = '<option value="owner"';
794 $option .= selected( 'owner', $selected_contact_type, false );
795 $option .= '>' . esc_html(__( 'Owners and Landlords', 'propertyhive' )) . '</option>';
796
797 $options[] = $option;
798
799 // Potential Owners
800 $option = '<option value="potentialowner"';
801 $option .= selected( 'potentialowner', $selected_contact_type, false );
802 $option .= '>' . esc_html(__( 'Potential Owners and Landlords', 'propertyhive' )) . '</option>';
803
804 $options[] = $option;
805
806 // Applicants
807 $option = '<option value="applicant"';
808 $option .= selected( 'applicant', $selected_contact_type, false );
809 $option .= '>' . esc_html(__( 'Applicants', 'propertyhive' )) . '</option>';
810
811 $options[] = $option;
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
820 // Third Parties
821 $option = '<option value="thirdparty"';
822 $option .= selected( 'thirdparty', $selected_contact_type, false );
823 $option .= '>' . esc_html(__( 'Third Party Contacts', 'propertyhive' )) . '</option>';
824
825 $options[] = $option;
826
827 $options = apply_filters( 'propertyhive_contact_filter_options', $options );
828
829 $output = '';
830 if (count($options) > 1)
831 {
832 $output = '<select name="_contact_type" id="dropdown_contact_type">';
833
834 $output .= '<option value="">' . esc_html(__( 'Show all contact types', 'propertyhive' )) . '</option>';
835
836 $output .= implode("", $options);
837
838 $output .= '</select>';
839 }
840
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 );
845 }
846
847 /**
848 * Show an enquiry filter box
849 */
850 public function enquiry_filters() {
851 global $wp_query;
852
853 // Department filtering
854 $output = '';
855
856 $output .= $this->date_range_filter();
857 $output .= $this->enquiry_status_filter();
858 $output .= $this->enquiry_source_filter();
859 $output .= $this->enquiry_office_filter();
860 $output .= $this->enquiry_negotiator_filter();
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.
863 echo apply_filters( 'propertyhive_enquiry_filters', $output );
864 }
865
866 /**
867 * Show an enquiry status filter box
868 */
869 public function enquiry_status_filter() {
870 global $wp_query;
871
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
876 // Status filtering
877 $output = '<select name="_status" id="dropdown_enquiry_status">
878 <option value="all"' . selected( 'all', $selected_status, false ) . '>All</option>';
879
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
894 $output .= '</select>';
895
896 return $output;
897 }
898
899 /**
900 * Show an enquiry source filter box
901 */
902 public function enquiry_source_filter() {
903 global $wp_query;
904
905 $sources = array(
906 'office' => __( 'Office', 'propertyhive' ),
907 'website' => __( 'Website', 'propertyhive' )
908 );
909
910 $sources = apply_filters( 'propertyhive_enquiry_sources', $sources );
911
912 asort($sources);
913
914 // Status filtering
915 $output = '<select name="_source" id="dropdown_enquiry_source">';
916 $selected_source = $this->get_admin_query_value( '_source' );
917
918 $output .= '<option value="">' . esc_html__( 'Show all sources', 'propertyhive' ) . '</option>';
919
920 foreach ( $sources as $key => $value )
921 {
922 $output .= '<option value="' . esc_attr($key) . '"';
923 if ( ! empty( $selected_source ) )
924 {
925 $output .= selected( $key, $selected_source, false );
926 }
927 $output .= '>' . esc_html( $value ) . '</option>';
928 }
929
930 $output .= '</select>';
931
932 return $output;
933 }
934
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 /**
995 * Show am appraisal filter box
996 */
997 public function appraisal_filters() {
998 global $wp_query;
999
1000 $output = '';
1001
1002 $output .= $this->appraisal_status_filter();
1003 $output .= $this->negotiator_filter();
1004 $output .= $this->date_range_filter();
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.
1007 echo apply_filters( 'propertyhive_appraisal_filters', $output );
1008 }
1009
1010 /**
1011 * Show an appraisal status filter box
1012 */
1013 public function appraisal_status_filter() {
1014 global $wp_query;
1015
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 : '';
1019
1020 // Status filtering
1021 $output = '<select name="_status" id="dropdown_appraisal_status">';
1022
1023 $output .= '<option value="">' . esc_html__( 'All Statuses', 'propertyhive' ) . '</option>';
1024
1025 $output .= '<option value="pending"';
1026 $output .= selected( 'pending', $selected_status, false );
1027 $output .= '>' . esc_html(__( 'Pending', 'propertyhive' )) . '</option>';
1028
1029 $output .= '<option value="carried_out"';
1030 $output .= selected( 'carried_out', $selected_status, false );
1031 $output .= '>' . esc_html(__( 'Carried Out', 'propertyhive' )) . '</option>';
1032
1033 $output .= '<option value="won"';
1034 $output .= selected( 'won', $selected_status, false );
1035 $output .= '>- ' . esc_html(__( 'Won', 'propertyhive' )) . '</option>';
1036
1037 $output .= '<option value="lost"';
1038 $output .= selected( 'lost', $selected_status, false );
1039 $output .= '>- ' . esc_html(__( 'Lost', 'propertyhive' )) . '</option>';
1040
1041 $output .= '<option value="instructed"';
1042 $output .= selected( 'instructed', $selected_status, false );
1043 $output .= '>- ' . esc_html(__( 'Instructed', 'propertyhive' )) . '</option>';
1044
1045 $output .= '<option value="cancelled"';
1046 $output .= selected( 'cancelled', $selected_status, false );
1047 $output .= '>' . esc_html(__( 'Cancelled', 'propertyhive' )) . '</option>';
1048
1049 $output .= '</select>';
1050
1051 return $output;
1052 }
1053
1054 /**
1055 * Show a viewing filter box
1056 */
1057 public function viewing_filters() {
1058 global $wp_query;
1059
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 : '';
1081
1082 // Status filtering
1083 $output = '<select name="_status" id="dropdown_viewing_status">';
1084
1085 $output .= '<option value="">' . esc_html__( 'All Statuses', 'propertyhive' ) . '</option>';
1086
1087 $viewing_statuses = ph_get_viewing_statuses();
1088
1089 foreach ( $viewing_statuses as $status => $display_status )
1090 {
1091 $output .= '<option value="' . esc_attr($status) . '"';
1092 $output .= selected( $status, $selected_status, false );
1093 $output .= '>' . esc_html($display_status) . '</option>';
1094 }
1095
1096 $output .= '</select>';
1097
1098 return $output;
1099 }
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
1128 /**
1129 * Show an offer filter box
1130 */
1131 public function offer_filters() {
1132 global $wp_query;
1133
1134 $output = '';
1135
1136 $output .= $this->offer_status_filter();
1137 $output .= $this->property_office_filter();
1138 $output .= $this->date_range_filter();
1139
1140 // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Built-in controls escape their text and attributes before this trusted PHP filter adds complete HTML controls.
1141 echo apply_filters( 'propertyhive_offer_filters', $output );
1142 }
1143
1144 /**
1145 * Show an offer status filter box
1146 */
1147 public function offer_status_filter() {
1148 global $wp_query;
1149
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 : '';
1153
1154 // Status filtering
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">';
1201
1202 $output .= '<option value="">' . esc_html__( 'All Statuses', 'propertyhive' ) . '</option>';
1203
1204 $sale_statuses = ph_get_sale_statuses();
1205
1206 foreach ( $sale_statuses as $status => $display_status )
1207 {
1208 $output .= '<option value="' . esc_attr($status) . '"';
1209 $output .= selected( $status, $selected_status, false );
1210 $output .= '>' . esc_html($display_status) . '</option>';
1211 }
1212
1213 $output .= '</select>';
1214
1215 return $output;
1216 }
1217
1218 /**
1219 * Show an tenancy filter box
1220 */
1221 public function tenancy_filters() {
1222 global $wp_query;
1223
1224 $output = '';
1225
1226 $output .= $this->tenancy_status_filter();
1227 $output .= $this->tenancy_management_type_filter();
1228
1229 // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Built-in controls escape their text and attributes before this trusted PHP filter adds complete HTML controls.
1230 echo apply_filters( 'propertyhive_tenancy_filters', $output );
1231 }
1232
1233 /**
1234 * Show an tenancy status filter box
1235 */
1236 public function tenancy_status_filter() {
1237 global $wp_query;
1238
1239 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only admin list display or query; no state change.
1240 $requested_value = isset( $_GET['_status'] ) && is_string( $_GET['_status'] ) ? sanitize_text_field( wp_unslash( $_GET['_status'] ) ) : '';
1241 $selected_status = in_array( $requested_value, array( 'pending', 'current', 'finished'), true ) ? $requested_value : '';
1242
1243 // Status filtering
1244 $output = '<select name="_status" id="dropdown_tenancy_status">';
1245
1246 $output .= '<option value="">' . esc_html(__( 'All Statuses', 'propertyhive' )) . '</option>';
1247
1248 $output .= '<option value="pending"';
1249 $output .= selected( 'pending', $selected_status, false );
1250 $output .= '>' . esc_html(__( 'Pending', 'propertyhive' )) . '</option>';
1251
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
1260 $output .= '</select>';
1261
1262 return $output;
1263 }
1264
1265 /**
1266 * Show an tenancy management type filter box
1267 */
1268 public function tenancy_management_type_filter() {
1269 global $wp_query;
1270
1271 $management_types = apply_filters( 'propertyhive_tenancy_management_types', array(
1272 'let_only' => 'Let Only',
1273 'fully_managed' => 'Fully Managed'
1274 ) );
1275
1276 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only admin list display or query; no state change.
1277 $requested_value = isset( $_GET['_management_type'] ) && is_string( $_GET['_management_type'] ) ? sanitize_text_field( wp_unslash( $_GET['_management_type'] ) ) : '';
1278 $selected_management_type = array_key_exists( $requested_value, $management_types ) ? $requested_value : '';
1279
1280 // Status filtering
1281 $output = '<select name="_management_type" id="dropdown_tenancy_management_type">';
1282
1283 $output .= '<option value="">' . esc_html(__( 'All Management Types', 'propertyhive' )) . '</option>';
1284
1285 foreach ( $management_types as $key => $value )
1286 {
1287 $output .= '<option value="' . esc_attr($key) . '"';
1288 $output .= selected( $key, $selected_management_type, false );
1289 $output .= '>' . esc_html( $value ) . '</option>';
1290 }
1291
1292 $output .= '</select>';
1293
1294 return $output;
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 }
1380
1381 /**
1382 * Filters and sorting handler
1383 * @param array $vars
1384 * @return array
1385 */
1386 public function request_query( $vars ) {
1387 global $typenow, $wp_query;
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.
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.
1392 if ( !isset($vars['tax_query']) ) { $vars['tax_query'] = array(); }
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
1402 if ( 'property' === $typenow )
1403 {
1404 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only admin list display or query; no state change.
1405 if ( ! empty( $department ) ) {
1406 $vars['meta_query'][] = array(
1407 'key' => '_department',
1408 'value' => $department,
1409 );
1410 }
1411 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only admin list display or query; no state change.
1412 if ( ! empty( $_GET['_office_id'] ) ) {
1413 $vars['meta_query'][] = array(
1414 'key' => '_office_id',
1415 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only admin list display or query; no state change.
1416 'value' => (int)$_GET['_office_id'],
1417 );
1418 }
1419 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only admin list display or query; no state change.
1420 if ( ! empty( $_GET['_negotiator_id'] ) ) {
1421 $vars['meta_query'][] = array(
1422 'key' => '_negotiator_id',
1423 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only admin list display or query; no state change.
1424 'value' => (int)$_GET['_negotiator_id'],
1425 );
1426 }
1427 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only admin list display or query; no state change.
1428 if ( ! empty( $_GET['_location_id'] ) ) {
1429 $vars['tax_query'][] = array(
1430 'taxonomy' => 'location',
1431 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only admin list display or query; no state change.
1432 'terms' => ( (is_array($_GET['_location_id'])) ? (int)$_GET['_location_id'] : array( (int)$_GET['_location_id'] ) )
1433 );
1434 }
1435 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only admin list display or query; no state change.
1436 if ( ! empty( $_GET['_availability_id'] ) ) {
1437 $vars['tax_query'][] = array(
1438 'taxonomy' => 'availability',
1439 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only admin list display or query; no state change.
1440 'terms' => ( (is_array($_GET['_availability_id'])) ? (int)$_GET['_availability_id'] : array( (int)$_GET['_availability_id'] ) )
1441 );
1442 }
1443 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only admin list display or query; no state change.
1444 if ( 'on_market' === $marketing ) {
1445 $vars['meta_query'][] = array(
1446 'key' => '_on_market',
1447 'value' => 'yes',
1448 );
1449 }
1450 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only admin list display or query; no state change.
1451 if ( 'off_market' === $marketing ) {
1452 $vars['meta_query'][] = array(
1453 'key' => '_on_market',
1454 'value' => 'yes',
1455 'compare' => '!=',
1456 );
1457 }
1458 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only admin list display or query; no state change.
1459 if ( 'featured' === $marketing ) {
1460 $vars['meta_query'][] = array(
1461 'key' => '_featured',
1462 'value' => 'yes',
1463 );
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 );
1468 $vars['tax_query'][] = array(
1469 'taxonomy' => 'marketing_flag',
1470 'terms' => ( (is_array($marketing_flag_id)) ? $marketing_flag_id : array( $marketing_flag_id ) )
1471 );
1472 }
1473 }
1474 elseif ( 'contact' === $typenow )
1475 {
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 }
1488 $vars['meta_query'][] = array(
1489 'key' => '_contact_types',
1490 'value' => $contact_type,
1491 'compare' => 'LIKE'
1492 );
1493 }
1494
1495 $vars = $this->filter_by_date_range($vars, 'date_query');
1496 }
1497 elseif ( 'enquiry' === $typenow )
1498 {
1499 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only admin list display or query; no state change.
1500 if ( ! empty( $status ) && $status != 'all' ) {
1501
1502 $vars['meta_query'][] = array(
1503 'key' => '_status',
1504 'value' => $status,
1505 );
1506 }
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 ) ) {
1520 $vars['meta_query'][] = array(
1521 'key' => '_source',
1522 'value' => $source,
1523 );
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');
1543 }
1544 elseif ( 'appraisal' === $typenow )
1545 {
1546 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only admin list display or query; no state change.
1547 if ( ! empty( $status ) ) {
1548 switch ( $status )
1549 {
1550 case "confirmed":
1551 {
1552 $vars['meta_query'][] = array(
1553 'key' => '_status',
1554 'value' => 'pending',
1555 );
1556 $vars['meta_query'][] = array(
1557 'key' => '_all_confirmed',
1558 'value' => 'yes',
1559 );
1560 break;
1561 }
1562 case "unconfirmed":
1563 {
1564 $vars['meta_query'][] = array(
1565 'key' => '_status',
1566 'value' => 'pending',
1567 );
1568 $vars['meta_query'][] = array(
1569 'key' => '_all_confirmed',
1570 'value' => '',
1571 );
1572 break;
1573 }
1574 default:
1575 {
1576 $vars['meta_query'][] = array(
1577 'key' => '_status',
1578 'value' => $status,
1579 );
1580 }
1581 }
1582 }
1583 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only admin list display or query; no state change.
1584 if ( ! empty( $_GET['_negotiator_id'] ) )
1585 {
1586 $vars['meta_query'][] = array(
1587 'key' => '_negotiator_id',
1588 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only admin list display or query; no state change.
1589 'value' => (int)$_GET['_negotiator_id'],
1590 );
1591 }
1592
1593 $vars = $this->filter_by_date_range($vars);
1594 }
1595 elseif ( 'viewing' === $typenow )
1596 {
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 )
1647 {
1648 case 'pending' :
1649 $vars['meta_query'][] = array(
1650 'key' => '_start_date',
1651 'value' => gmdate('Y-m-d'),
1652 'type' => 'date',
1653 'compare' => '>',
1654 );
1655 break;
1656
1657 case 'current' :
1658 $vars['meta_query'][] = array(
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 )
1687 );
1688 break;
1689
1690 case 'finished':
1691 $vars['meta_query'][] = array(
1692 'key' => '_end_date',
1693 'value' => gmdate('Y-m-d'),
1694 'type' => 'date',
1695 'compare' => '<',
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':
1721 $vars['meta_query'][] = array(
1722 'key' => '_key_date_status',
1723 'value' => $value,
1724 );
1725 break;
1726 case 'pending':
1727 $vars['meta_query'][] = array(
1728 'key' => '_key_date_status',
1729 'value' => 'pending',
1730 );
1731 break;
1732 case 'overdue':
1733 $vars['meta_query'][] = array(
1734 'key' => '_key_date_status',
1735 'value' => array('pending', 'booked'),
1736 'compare' => 'IN'
1737 );
1738 $vars['meta_query'][] = array(
1739 'key' => '_date_due',
1740 'value' => gmdate("Y-m-d"),
1741 'type' => 'date',
1742 'compare' => '<',
1743 );
1744 break;
1745 case 'upcoming_and_overdue':
1746 $vars['meta_query'][] = array(
1747 'key' => '_key_date_status',
1748 'value' => array('pending', 'booked'),
1749 'compare' => 'IN'
1750 );
1751 $upcoming_threshold = new DateTime('+ ' . apply_filters( 'propertyhive_key_date_upcoming_days', 7 ) . ' DAYS');
1752 $vars['meta_query'][] = array(
1753 'key' => '_date_due',
1754 'value' => $upcoming_threshold->format('Y-m-d'),
1755 'type' => 'date',
1756 'compare' => '<=',
1757 );
1758 break;
1759 }
1760 }
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'] ) )
1764 {
1765 $vars['meta_query'][] = array(
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'],
1769 );
1770 }
1771
1772 $vars = $this->filter_by_date_range($vars, '_date_due');
1773 }
1774
1775 $vars = apply_filters( 'propertyhive_property_filter_query', $vars, $typenow );
1776
1777 return $vars;
1778 }
1779
1780 private function filter_by_date_range($vars, $meta_key = '_start_date_time')
1781 {
1782 $date_range_label = $this->get_admin_query_value( '_date_range_label' );
1783 $date_range_from = $this->get_admin_query_value( '_date_range_from' );
1784 $date_range_to = $this->get_admin_query_value( '_date_range_to' );
1785
1786 if (
1787 ! empty( $date_range_label )
1788 && ! empty( $date_range_from )
1789 && ! empty( $date_range_to )
1790 && $date_range_label !== 'Any Time'
1791 && DateTime::createFromFormat('Y-m-d', $date_range_from) !== false
1792 && DateTime::createFromFormat('Y-m-d', $date_range_to) !== false
1793 )
1794 {
1795 if ( $meta_key == 'date_query' )
1796 {
1797 $vars['date_query'] = array(
1798 'after' => $date_range_from . ' 00:00:00',
1799 'before' => $date_range_to . ' 23:59:59',
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
2021 /**
2022 * Removes variations etc belonging to a deleted post, and clears transients
2023 *
2024 * @access public
2025 * @param mixed $id ID of post being deleted
2026 * @return void
2027 */
2028 public function delete_post( $id ) {
2029 /*global $propertyhive, $wpdb;
2030
2031 if ( ! current_user_can( 'delete_posts' ) )
2032 return;
2033
2034 if ( $id > 0 ) {
2035
2036 $post_type = get_post_type( $id );
2037
2038 switch( $post_type ) {
2039 case 'property' :
2040 ph_delete_property_transients();
2041 break;
2042 case 'contact' :
2043 ph_delete_contact_transients();
2044 break;
2045 case 'enquiry' :
2046 ph_delete_enquiry_transients();
2047 break;
2048 }
2049 }*/
2050 }
2051
2052 /**
2053 * propertyhive_trash_post function.
2054 *
2055 * @access public
2056 * @param mixed $id
2057 * @return void
2058 */
2059 public function trash_post( $id ) {
2060 /*if ( $id > 0 ) {
2061
2062 $post_type = get_post_type( $id );
2063
2064
2065 }*/
2066 }
2067
2068 /**
2069 * propertyhive_untrash_post function.
2070 *
2071 * @access public
2072 * @param mixed $id
2073 * @return void
2074 */
2075 public function untrash_post( $id ) {
2076 /*if ( $id > 0 ) {
2077
2078 $post_type = get_post_type( $id );
2079
2080 }*/
2081 }
2082 }
2083
2084 endif;
2085
2086 return new PH_Admin_Post_Types();
2087