PluginProbe
Property Hive / trunk
Property Hive vtrunk
2.3.0 2.2.6 2.2.5 2.2.4 2.2.3 2.2.2 1.4.46 1.4.47 1.4.48 1.4.49 1.4.5 1.4.50 1.4.51 1.4.52 1.4.53 1.4.54 1.4.55 1.4.56 1.4.57 1.4.58 1.4.59 1.4.6 1.4.60 1.4.61 1.4.62 All 260 releases
propertyhive / includes / admin / class-ph-admin-post-types.php

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

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