PluginProbe
Property Hive / 2.3.1
Property Hive v2.3.1
2.3.1 2.3.0 2.2.6 2.2.5 2.2.4 2.2.3 2.2.2 1.4.46 1.4.47 1.4.48 1.4.49 1.4.5 1.4.50 1.4.51 1.4.52 1.4.53 1.4.54 1.4.55 1.4.56 1.4.57 1.4.58 1.4.59 1.4.6 1.4.60 1.4.61 All 261 releases
← All changes | includes/class-ph-shortcodes.php +1237 -103 1.4.462.3.1 View file →
@@ -1,6 +1,9 @@
1 1 <?php
2 +// phpcs:set WordPress.Security.ValidatedSanitizedInput customSanitizingFunctions[] ph_clean
3 +// ph_clean() recursively sanitizes text; presence, shape and unslashing checks remain separate.
2 4
5 +
3 6 if ( ! defined( 'ABSPATH' ) ) exit; // Exit if accessed directly
4 7
5 8 /**
6 9 * PH_Shortcodes class.
@@ -24,8 +27,9 @@
24 27 'featured_properties' => __CLASS__ . '::featured_properties',
25 28 'similar_properties' => __CLASS__ . '::similar_properties',
26 29 'property_search_form' => __CLASS__ . '::property_search_form',
27 30 'property_map' => __CLASS__ . '::property_map',
31 + 'property_static_map' => __CLASS__ . '::property_static_map',
28 32 'property_street_view' => __CLASS__ . '::property_street_view',
29 33 'property_office_details' => __CLASS__ . '::property_office_details',
30 34 'office_map' => __CLASS__ . '::office_map',
31 35 'applicant_registration_form' => __CLASS__ . '::applicant_registration_form',
@@ -30,11 +34,13 @@
30 34 'office_map' => __CLASS__ . '::office_map',
31 35 'applicant_registration_form' => __CLASS__ . '::applicant_registration_form',
32 36 'propertyhive_my_account' => __CLASS__ . '::my_account',
33 37 'propertyhive_login_form' => __CLASS__ . '::login_form',
38 + 'propertyhive_reset_password_form' => __CLASS__ . '::reset_password_form',
34 39 );
35 40
36 41 foreach ( $shortcodes as $shortcode => $function ) {
42 + // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.DynamicHooknameFound -- Public shortcode registration filter; extensions can customize each shortcode tag through this existing dynamic hook contract.
37 43 add_shortcode( apply_filters( "{$shortcode}_shortcode_tag", $shortcode ), $function );
38 44 }
39 45 }
40 46
@@ -58,10 +64,12 @@
58 64
59 65 $before = empty( $wrapper['before'] ) ? '<div class="' . esc_attr( $wrapper['class'] ) . '">' : $wrapper['before'];
60 66 $after = empty( $wrapper['after'] ) ? '</div>' : $wrapper['after'];
61 67
68 + // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Wrapper HTML is supplied by PHP callers; the default class is escaped when assembled.
62 69 echo $before;
63 70 call_user_func( $function, $atts );
71 + // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Closing wrapper HTML is supplied by PHP callers.
64 72 echo $after;
65 73
66 74 return ob_get_clean();
67 75 }
@@ -75,14 +83,14 @@
75 83 public static function property_search_form( $atts ) {
76 84 $atts = shortcode_atts( array(
77 85 'id' => 'shortcode',
78 86 'default_department' => ''
79 - ), $atts );
87 + ), $atts, 'property_search_form' );
80 88
81 89 $form_controls = ph_get_search_form_fields();
82 90
83 - $form_controls = apply_filters( 'propertyhive_search_form_fields_' . $atts['id'], $form_controls );
84 - $form_controls = apply_filters( 'propertyhive_search_form_fields', $form_controls );
91 + $form_controls = apply_filters( 'propertyhive_search_form_fields_' . $atts['id'], $form_controls, $atts );
92 + $form_controls = apply_filters( 'propertyhive_search_form_fields', $form_controls, $atts );
85 93
86 94 // We 100% need department so make sure it exists. If it doesn't, set a hidden field
87 95 if ( !isset($form_controls['department']) )
88 96 {
@@ -92,10 +100,14 @@
92 100
93 101 $form_controls['department'] = $original_department;
94 102 }
95 103
104 + $form_controls = apply_filters( 'propertyhive_search_form_fields_after_' . $atts['id'], $form_controls, $atts );
105 + $form_controls = apply_filters( 'propertyhive_search_form_fields_after', $form_controls, $atts );
106 +
96 107 if (
97 - isset($atts['default_department']) && in_array($atts['default_department'], array('residential-sales', 'residential-lettings', 'commercial')) &&
108 + isset($atts['default_department']) && in_array($atts['default_department'], array_keys( ph_get_departments() )) &&
109 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Public read-only search form default; no state change.
98 110 ( !isset($_REQUEST['department']) )
99 111 )
100 112 {
101 113 $form_controls['department']['value'] = $atts['default_department'];
@@ -114,23 +126,34 @@
114 126 * @param array $atts
115 127 * @return string
116 128 */
117 129 public static function properties( $atts ) {
130 +
131 + global $propertyhive_loop;
132 +
118 133 $atts = shortcode_atts( array(
119 134 'columns' => '2',
120 135 'orderby' => 'meta_value_num',
121 136 'order' => 'desc',
137 + // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- These lines declare shortcode defaults; they do not pass a meta_key/exclude value to WP_Query or get_posts. 131 is the properties shortcode_atts default meta_key; 133/751/1045/1338 are exclude defaults in shortcode_atts.
122 138 'meta_key' => '_price_actual',
123 139 'ids' => '',
124 - 'department' => '', // residential-sales / residential-lettings / commercial
140 + // phpcs:ignore WordPressVIPMinimum.Performance.WPQueryParams.PostNotIn_exclude -- These lines declare shortcode defaults; they do not pass a meta_key/exclude value to WP_Query or get_posts. 131 is the properties shortcode_atts default meta_key; 133/751/1045/1338 are exclude defaults in shortcode_atts.
141 + 'exclude' => '',
142 + 'department' => '', // residential-sales / residential-lettings / commercial / any custom department
125 143 'minimum_price' => '',
126 144 'maximum_price' => '',
127 145 'bedrooms' => '',
146 + 'minimum_bedrooms' => '',
147 + 'keyword' => '',
128 148 'address_keyword' => '',
149 + 'country' => '',
150 + 'country_not' => '',
129 151 'availability_id' => '',
130 152 'marketing_flag' => '', // Deprecated. Use marketing_flag_id instead
131 153 'marketing_flag_id' => '', // Should be marketing_flag_id. Might deprecate this in the future
132 154 'property_type_id' => '',
155 + 'sale_by_id' => '',
133 156 'location_id' => '',
134 157 'office_id' => '',
135 158 'negotiator_id' => '',
136 159 'commercial_for_sale' => '',
@@ -136,10 +159,39 @@
136 159 'commercial_for_sale' => '',
137 160 'commercial_to_rent' => '',
138 161 'posts_per_page' => 10,
139 162 'no_results_output' => '',
163 + 'pagination' => '',
164 + 'show_order' => '',
165 + 'show_result_count' => '',
166 + 'carousel' => '',
140 167 ), $atts, 'properties' );
141 168
169 + if ( isset($atts['carousel']) && !empty($atts['carousel']) )
170 + {
171 + $params = array(
172 + 'items' => 1,
173 + 'controlsPosition' => 'bottom',
174 + 'gutter' => 20,
175 + 'mouseDrag' => true,
176 + 'nav' => false,
177 + 'navPosition' => 'bottom',
178 + 'controlsText' => array("Prev", "Next"),
179 + 'responsive' => array(
180 + 640 => array(
181 + 'items' => (int)$atts['columns']
182 + )
183 + )
184 + );
185 + $params = apply_filters( 'propertyhive_carousel_params', $params );
186 + $params = apply_filters( 'propertyhive_properties_carousel_params', $params );
187 + wp_localize_script( 'propertyhive_carousel', 'propertyhive_carousel_params', $params );
188 +
189 + wp_enqueue_style( 'tiny_slider_css' );
190 + wp_enqueue_script( 'tiny_slider' );
191 + wp_enqueue_script( 'propertyhive_carousel' );
192 + }
193 +
142 194 $meta_query = array(
143 195 array(
144 196 'key' => '_on_market',
145 197 'value' => 'yes',
@@ -145,14 +197,18 @@
145 197 'value' => 'yes',
146 198 )
147 199 );
148 200
149 - if ( isset($atts['department']) && in_array($atts['department'], array("residential-sales", "residential-lettings", "commercial")) )
201 + if ( isset($atts['department']) && in_array($atts['department'], array_keys( ph_get_departments() )) )
150 202 {
203 + $departments = explode(",", $atts['department']);
204 + $departments = array_map('trim', $departments);
205 + $departments = array_filter($departments);
206 +
151 207 $meta_query[] = array(
152 208 'key' => '_department',
153 - 'value' => $atts['department'],
154 - 'compare' => '='
209 + 'value' => $departments,
210 + 'compare' => 'IN'
155 211 );
156 212 }
157 213
158 214 if ( isset($atts['bedrooms']) && $atts['bedrooms'] != '' && is_numeric($atts['bedrooms']) )
@@ -159,13 +215,30 @@
159 215 {
160 216 $meta_query[] = array(
161 217 'key' => '_bedrooms',
162 218 'value' => sanitize_text_field( $atts['bedrooms'] ),
163 - 'compare' => '='
219 + 'compare' => '=',
220 + 'type' => 'NUMERIC'
164 221 );
165 222 }
166 223
167 - if ( isset($atts['department']) && $atts['department'] == 'residential-sales' && isset($atts['minimum_price']) && $atts['minimum_price'] != '' )
224 + if ( isset($atts['minimum_bedrooms']) && $atts['minimum_bedrooms'] != '' && is_numeric($atts['minimum_bedrooms']) )
225 + {
226 + $meta_query[] = array(
227 + 'key' => '_bedrooms',
228 + 'value' => sanitize_text_field( $atts['minimum_bedrooms'] ),
229 + 'compare' => '>=',
230 + 'type' => 'NUMERIC'
231 + );
232 + }
233 +
234 + $base_department = $atts['department'];
235 + if ( $atts['department'] !== '' && !in_array($atts['department'], array_keys( ph_get_departments( true ) )) )
236 + {
237 + $base_department = ph_get_custom_department_based_on($base_department);
238 + }
239 +
240 + if ( isset($atts['department']) && ( $base_department == 'residential-sales' || $base_department == 'residential-lettings' ) && isset($atts['minimum_price']) && $atts['minimum_price'] != '' )
168 241 {
169 242 $search_form_currency = get_option( 'propertyhive_search_form_currency', 'GBP' );
170 243
171 244 $minimum_price = $atts['minimum_price'];
@@ -184,9 +257,9 @@
184 257 'type' => 'NUMERIC'
185 258 );
186 259 }
187 260
188 - if ( isset($atts['department']) && $atts['department'] == 'residential-sales' && isset($atts['maximum_price']) && $atts['maximum_price'] != '' )
261 + if ( isset($atts['department']) && ( $base_department == 'residential-sales' || $base_department == 'residential-lettings' ) && isset($atts['maximum_price']) && $atts['maximum_price'] != '' )
189 262 {
190 263 $search_form_currency = get_option( 'propertyhive_search_form_currency', 'GBP' );
191 264
192 265 $maximum_price = $atts['maximum_price'];
@@ -207,9 +280,9 @@
207 280 }
208 281
209 282 if ( isset($atts['address_keyword']) && $atts['address_keyword'] != '' )
210 283 {
211 - $atts['address_keyword'] = sanitize_text_field( trim( $atts['address_keyword'] ) );
284 + $atts['address_keyword'] = ph_clean( trim( $atts['address_keyword'] ) );
212 285
213 286 $address_keywords = array( $atts['address_keyword'] );
214 287
215 288 if ( strpos( $atts['address_keyword'], ' ' ) !== FALSE )
@@ -219,37 +292,55 @@
219 292 if ( strpos( $atts['address_keyword'], '-' ) !== FALSE )
220 293 {
221 294 $address_keywords[] = str_replace("-", " ", $atts['address_keyword']);
222 295 }
296 + if ( strpos( $atts['address_keyword'], '.' ) !== FALSE )
297 + {
298 + $address_keywords[] = str_replace(".", "", $atts['address_keyword']);
299 + }
300 + if ( stripos( $atts['address_keyword'], 'st ' ) !== FALSE )
301 + {
302 + $address_keywords[] = str_ireplace("st ", "st. ", $atts['address_keyword']);
303 + }
304 + if ( strpos( $atts['address_keyword'], '\'' ) !== FALSE )
305 + {
306 + $address_keywords[] = str_replace("'", "", $atts['address_keyword']);
307 + }
223 308
224 309 $sub_meta_query = array('relation' => 'OR');
225 310
311 + $address_keyword_compare = get_option( 'propertyhive_address_keyword_compare', '=' );
312 + if ( $address_keyword_compare == 'polygon' )
313 + {
314 + $address_keyword_compare = apply_filters('propertyhive_shortcode_address_keyword_compare', '=');
315 + }
316 +
226 317 foreach ( $address_keywords as $address_keyword )
227 318 {
228 319 $sub_meta_query[] = array(
229 320 'key' => '_reference_number',
230 321 'value' => $address_keyword,
231 - 'compare' => get_option( 'propertyhive_address_keyword_compare', '=' )
322 + 'compare' => $address_keyword_compare
232 323 );
233 324 $sub_meta_query[] = array(
234 325 'key' => '_address_street',
235 326 'value' => $address_keyword,
236 - 'compare' => get_option( 'propertyhive_address_keyword_compare', '=' )
327 + 'compare' => $address_keyword_compare
237 328 );
238 329 $sub_meta_query[] = array(
239 330 'key' => '_address_two',
240 331 'value' => $address_keyword,
241 - 'compare' => get_option( 'propertyhive_address_keyword_compare', '=' )
332 + 'compare' => $address_keyword_compare
242 333 );
243 334 $sub_meta_query[] = array(
244 335 'key' => '_address_three',
245 336 'value' => $address_keyword,
246 - 'compare' => get_option( 'propertyhive_address_keyword_compare', '=' )
337 + 'compare' => $address_keyword_compare
247 338 );
248 339 $sub_meta_query[] = array(
249 340 'key' => '_address_four',
250 341 'value' => $address_keyword,
251 - 'compare' => get_option( 'propertyhive_address_keyword_compare', '=' )
342 + 'compare' => $address_keyword_compare
252 343 );
253 344 }
254 345 if ( strlen($atts['address_keyword']) <= 4 )
255 346 {
@@ -257,11 +348,14 @@
257 348 'key' => '_address_postcode',
258 349 'value' => sanitize_text_field( $atts['address_keyword'] ),
259 350 'compare' => '='
260 351 );
352 + // Run regex match where given keyword is at the start of the postcode ^
353 + // followed by one or zero letters (for WC2E-style postcodes) [a-zA-Z]?
354 + // then a single space [ ]
261 355 $sub_meta_query[] = array(
262 356 'key' => '_address_postcode',
263 - 'value' => sanitize_text_field( $atts['address_keyword'] ) . '[ ]',
357 + 'value' => sanitize_text_field( $atts['address_keyword'] ) . '[a-zA-Z]?[ ]',
264 358 'compare' => 'RLIKE'
265 359 );
266 360 }
267 361 else
@@ -275,8 +369,135 @@
275 369
276 370 $meta_query[] = $sub_meta_query;
277 371 }
278 372
373 + if ( isset($atts['keyword']) && $atts['keyword'] != '' )
374 + {
375 + $atts['keyword'] = sanitize_text_field( trim( $atts['keyword'] ) );
376 +
377 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Preserve the original request verbatim only to restore it after this shortcode's query; it is not output or persisted.
378 + $original_keyword = isset($_REQUEST['keyword']) ? $_REQUEST['keyword'] : '';
379 + $_REQUEST['keyword'] = $atts['keyword'];
380 +
381 + add_filter( 'posts_where', array( PH()->query, 'keyword_excerpt_where' ), 10, 2 );
382 +
383 + $keywords = array( $atts['keyword'] );
384 +
385 + if ( strpos( $atts['keyword'], ' ' ) !== FALSE )
386 + {
387 + $keywords[] = str_replace(" ", "-", $atts['keyword']);
388 + }
389 + if ( strpos( $atts['keyword'], '-' ) !== FALSE )
390 + {
391 + $keywords[] = str_replace("-", " ", $atts['keyword']);
392 + }
393 + if ( strpos( $atts['keyword'], '.' ) !== FALSE )
394 + {
395 + $keywords[] = str_replace(".", "", $atts['keyword']);
396 + }
397 + if ( stripos( $atts['keyword'], 'st ' ) !== FALSE )
398 + {
399 + $keywords[] = str_ireplace("st ", "st. ", $atts['keyword']);
400 + }
401 + if ( strpos( $atts['keyword'], '\'' ) !== FALSE )
402 + {
403 + $keywords[] = str_replace("'", "", $atts['keyword']);
404 + }
405 +
406 + $sub_meta_query = array('relation' => 'OR');
407 +
408 + $address_keyword_compare = get_option( 'propertyhive_address_keyword_compare', '=' );
409 + if ( $address_keyword_compare == 'polygon' )
410 + {
411 + $address_keyword_compare = apply_filters('propertyhive_shortcode_address_keyword_compare', '=');
412 + }
413 +
414 + foreach ( $keywords as $keyword )
415 + {
416 + $sub_meta_query[] = array(
417 + 'key' => '_reference_number',
418 + 'value' => $keyword,
419 + 'compare' => $address_keyword_compare
420 + );
421 + $sub_meta_query[] = array(
422 + 'key' => '_address_street',
423 + 'value' => $keyword,
424 + 'compare' => $address_keyword_compare
425 + );
426 + $sub_meta_query[] = array(
427 + 'key' => '_address_two',
428 + 'value' => $keyword,
429 + 'compare' => $address_keyword_compare
430 + );
431 + $sub_meta_query[] = array(
432 + 'key' => '_address_three',
433 + 'value' => $keyword,
434 + 'compare' => $address_keyword_compare
435 + );
436 + $sub_meta_query[] = array(
437 + 'key' => '_address_four',
438 + 'value' => $keyword,
439 + 'compare' => $address_keyword_compare
440 + );
441 + $sub_meta_query[] = array(
442 + 'key' => '_features_concatenated',
443 + 'value' => $keyword,
444 + 'compare' => 'LIKE'
445 + );
446 + $sub_meta_query[] = array(
447 + 'key' => '_descriptions_concatenated',
448 + 'value' => $keyword,
449 + 'compare' => 'LIKE'
450 + );
451 + }
452 + if ( strlen($atts['keyword']) <= 4 )
453 + {
454 + $sub_meta_query[] = array(
455 + 'key' => '_address_postcode',
456 + 'value' => sanitize_text_field( $atts['keyword'] ),
457 + 'compare' => '='
458 + );
459 + // Run regex match where given keyword is at the start of the postcode ^
460 + // followed by one or zero letters (for WC2E-style postcodes) [a-zA-Z]?
461 + // then a single space [ ]
462 + $sub_meta_query[] = array(
463 + 'key' => '_address_postcode',
464 + 'value' => sanitize_text_field( $atts['keyword'] ) . '[a-zA-Z]?[ ]',
465 + 'compare' => 'RLIKE'
466 + );
467 + }
468 + else
469 + {
470 + $sub_meta_query[] = array(
471 + 'key' => '_address_postcode',
472 + 'value' => sanitize_text_field( $atts['keyword'] ),
473 + 'compare' => 'LIKE'
474 + );
475 + }
476 +
477 + $meta_query[] = $sub_meta_query;
478 +
479 + $_REQUEST['keyword'] = $original_keyword; // reset back in case it's used elsewhere
480 + }
481 +
482 + if ( isset($atts['country']) && $atts['country'] != '' )
483 + {
484 + $meta_query[] = array(
485 + 'key' => '_address_country',
486 + 'value' => sanitize_text_field( $atts['country'] ),
487 + 'compare' => '=',
488 + );
489 + }
490 +
491 + if ( isset($atts['country_not']) && $atts['country_not'] != '' )
492 + {
493 + $meta_query[] = array(
494 + 'key' => '_address_country',
495 + 'value' => sanitize_text_field( $atts['country_not'] ),
496 + 'compare' => '!=',
497 + );
498 + }
499 +
279 500 if ( isset($atts['office_id']) && $atts['office_id'] != '' )
280 501 {
281 502 $meta_query[] = array(
282 503 'key' => '_office_id',
@@ -338,9 +559,18 @@
338 559 }
339 560
340 561 if ( isset($atts['property_type_id']) && $atts['property_type_id'] != '' )
341 562 {
342 - if ( isset($atts['department']) && $atts['department'] == 'commercial' )
563 + // Change field to check when department is specified as commercial, or if commercial is the only active department
564 + if (
565 + ( isset($atts['department']) && $base_department == 'commercial' ) ||
566 + (
567 + !isset($atts['department']) &&
568 + get_option( 'propertyhive_active_departments_sales' ) != 'yes' &&
569 + get_option( 'propertyhive_active_departments_lettings' ) != 'yes' &&
570 + get_option( 'propertyhive_active_departments_commercial' ) == 'yes'
571 + )
572 + )
343 573 {
344 574 $tax_query[] = array(
345 575 'taxonomy' => 'commercial_property_type',
346 576 'terms' => explode(",", $atts['property_type_id']),
@@ -365,11 +595,20 @@
365 595 'compare' => 'IN',
366 596 );
367 597 }
368 598
599 + if ( isset($atts['sale_by_id']) && $atts['sale_by_id'] != '' )
600 + {
601 + $tax_query[] = array(
602 + 'taxonomy' => 'sale_by',
603 + 'terms' => explode(",", $atts['sale_by_id']),
604 + 'compare' => 'IN',
605 + );
606 + }
607 +
369 608 // Change default meta key when department is specified as commercial, or if commercial is the only active department
370 609 if (
371 - ( isset($atts['department']) && $atts['department'] == 'commercial' ) ||
610 + ( isset($atts['department']) && $base_department == 'commercial' ) ||
372 611 (
373 612 get_option( 'propertyhive_active_departments_sales' ) != 'yes' &&
374 613 get_option( 'propertyhive_active_departments_lettings' ) != 'yes' &&
375 614 get_option( 'propertyhive_active_departments_commercial' ) == 'yes'
@@ -375,11 +614,15 @@
375 614 get_option( 'propertyhive_active_departments_commercial' ) == 'yes'
376 615 )
377 616 )
378 617 {
618 + // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- Property sorting uses the established price, floor-area or market-date metadata; retain the selected ordering and existing query limits.
379 619 $atts['meta_key'] = '_floor_area_from_sqft';
380 620 }
381 621
622 + // Get which page we're currently viewing from the URL
623 + $paged = max( 1, get_query_var( 'paged' ) );
624 +
382 625 $args = array(
383 626 'post_type' => 'property',
384 627 'post_status' => ( ( is_user_logged_in() && current_user_can( 'manage_propertyhive' ) ) ? array('publish', 'private') : 'publish' ),
385 628 'ignore_sticky_posts' => 1,
@@ -385,28 +628,92 @@
385 628 'ignore_sticky_posts' => 1,
386 629 'orderby' => $atts['orderby'],
387 630 'order' => $atts['order'],
388 631 'posts_per_page' => $atts['posts_per_page'],
632 + 'paged' => $paged,
633 + // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- Property eligibility and matching fields live in the established metadata schema; retain these filters and the shortcode page limit.
389 634 'meta_query' => $meta_query,
390 - 'tax_query' => $tax_query
635 + // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query -- Property taxonomy filters are required by this shortcode; WordPress builds the query and the shortcode page limit is retained.
636 + 'tax_query' => $tax_query,
637 + 'has_password' => false,
391 638 );
392 639 if ( ! empty( $atts['meta_key'] ) ) {
640 + // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- Property sorting uses the established price, floor-area or market-date metadata; retain the selected ordering and existing query limits.
393 641 $args['meta_key'] = $atts['meta_key'];
394 642 }
395 643
396 - if ( ! empty( $atts['ids'] ) ) {
397 - $args['post__in'] = array_map( 'trim', explode( ',', $atts['ids'] ) );
644 + if ( ! empty( $atts['ids'] ) )
645 + {
646 + $include_ids = array_map( 'absint', explode( ',', $atts['ids'] ) );
647 + $include_ids = array_filter( $include_ids );
648 + if ( ! empty( $include_ids ) )
649 + {
650 + $args['post__in'] = $include_ids;
651 + }
398 652 }
653 + if ( ! empty( $atts['exclude'] ) )
654 + {
655 + $exclude_ids = array_map( 'absint', explode( ',', $atts['exclude'] ) );
656 + $exclude_ids = array_filter( $exclude_ids );
657 + if ( ! empty( $exclude_ids ) ) {
658 + // phpcs:ignore WordPressVIPMinimum.Performance.WPQueryParams.PostNotIn_post__not_in -- Explicit shortcode exclusions are validated as integer IDs above; retain this published selection feature within the shortcode page limit.
659 + $args['post__not_in'] = $exclude_ids;
660 + }
661 + }
662 + if ( isset($atts['orderby']) && $atts['orderby'] == 'date' )
663 + {
664 + $args['orderby'] = 'meta_value';
665 + // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- Property sorting uses the established price, floor-area or market-date metadata; retain the selected ordering and existing query limits.
666 + $args['meta_key'] = '_on_market_change_date';
667 + }
399 668
669 + $args['orderby'] .= ' post_title';
670 +
400 671 ob_start();
401 672
402 - $properties = new WP_Query( apply_filters( 'propertyhive_properties_query', $args, $atts ) );
673 + do_action('propertyhive_shortcode_properties_before_catalog_ordering', $atts);
403 674
404 - $propertyhive_loop['columns'] = $atts['columns'];
675 + if ( isset($atts['show_order']) && $atts['show_order'] != '' )
676 + {
677 + list( $args, $orderby ) = self::get_show_order_args( $atts, $args );
405 678
679 + propertyhive_catalog_ordering( $atts['department'], $orderby );
680 + }
681 +
682 + do_action('propertyhive_shortcode_properties_after_catalog_ordering', $atts);
683 +
684 + $args = apply_filters( 'propertyhive_properties_query', $args, $atts );
685 + $args = apply_filters( 'propertyhive_shortcode_properties_query', $args, $atts );
686 +
687 + $properties = new WP_Query( $args );
688 +
689 + if ( isset($atts['show_result_count']) && $atts['show_result_count'] != '' )
690 + {
691 + $total_posts = $properties->found_posts;
692 +
693 + $first = ( $atts['posts_per_page'] * $paged ) - $atts['posts_per_page'] + 1;
694 + $last = min( $total_posts, $atts['posts_per_page'] * $paged );
695 +
696 + propertyhive_result_count( $paged, $atts['posts_per_page'], $total_posts, $first, $last);
697 + }
698 +
699 + do_action('propertyhive_shortcode_properties_after_result_count', $atts);
700 +
701 + $propertyhive_loop['columns'] = (int)$atts['columns'];
702 +
406 703 if ( $properties->have_posts() ) : ?>
407 704
408 - <?php propertyhive_property_loop_start(); ?>
705 + <?php
706 + ob_start();
707 + propertyhive_property_loop_start();
708 + $loop_start = ob_get_clean();
709 + if ( isset($atts['carousel']) && !empty($atts['carousel']) )
710 + {
711 + $loop_start = str_replace("class=\"properties", "class=\"properties propertyhive-shortcode-carousel", $loop_start);
712 + }
713 + // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Buffered loop template HTML; preserve theme overrides and the static carousel class insertion.
714 + echo $loop_start;
715 + ?>
409 716
410 717 <?php while ( $properties->have_posts() ) : $properties->the_post(); ?>
411 718
412 719 <?php ph_get_template_part( 'content', 'property' ); ?>
@@ -416,17 +723,22 @@
416 723 <?php propertyhive_property_loop_end(); ?>
417 724
418 725 <?php else: ?>
419 726
420 - <?php echo $atts['no_results_output']; ?>
727 + <p class="propertyhive-info no-results-message"><?php echo wp_kses_post($atts['no_results_output']); ?></p>
421 728
422 729 <?php endif;
423 730
731 + if ( isset($atts['pagination']) && $atts['pagination'] != '' )
732 + {
733 + propertyhive_pagination( $properties->max_num_pages );
734 + }
735 +
424 736 wp_reset_postdata();
425 737
426 738 $shortcode_output = ob_get_clean();
427 739
428 - return apply_filters( 'propertyhive_properties_shortcode_output', '<div class="propertyhive propertyhive-properties-shortcode columns-' . $atts['columns'] . '">' . $shortcode_output . '</div>', $shortcode_output );
740 + return apply_filters( 'propertyhive_properties_shortcode_output', '<div class="propertyhive propertyhive-properties-shortcode columns-' . (int)$atts['columns'] . '">' . $shortcode_output . '</div>', $shortcode_output );
429 741 }
430 742
431 743 /**
432 744 * Recent Properties shortcode
@@ -441,26 +753,96 @@
441 753 $atts = shortcode_atts( array(
442 754 'per_page' => '12',
443 755 'columns' => '4',
444 756 'department' => '',
757 + 'minimum_price' => '',
445 758 'office_id' => '',
759 + 'negotiator_id' => '',
446 760 'availability_id' => '',
761 + 'marketing_flag_id' => '',
762 + 'property_type_id' => '',
763 + 'sale_by_id' => '',
764 + 'location_id' => '',
765 + 'commercial_for_sale' => '',
766 + 'commercial_to_rent' => '',
767 + // phpcs:ignore WordPressVIPMinimum.Performance.WPQueryParams.PostNotIn_exclude -- These lines declare shortcode defaults; they do not pass a meta_key/exclude value to WP_Query or get_posts. 131 is the properties shortcode_atts default meta_key; 133/751/1045/1338 are exclude defaults in shortcode_atts.
768 + 'exclude' => '',
447 769 'orderby' => 'date',
448 770 'order' => 'desc',
449 771 'no_results_output' => '',
772 + 'pagination' => '',
773 + 'show_order' => '',
774 + 'show_result_count' => '',
775 + 'carousel' => '',
450 776 ), $atts, 'recent_properties' );
451 777
778 + if ( isset($atts['carousel']) && !empty($atts['carousel']) )
779 + {
780 + $params = array(
781 + 'items' => 1,
782 + 'controlsPosition' => 'bottom',
783 + 'gutter' => 20,
784 + 'mouseDrag' => true,
785 + 'nav' => false,
786 + 'navPosition' => 'bottom',
787 + 'controlsText' => array("Prev", "Next"),
788 + 'responsive' => array(
789 + 640 => array(
790 + 'items' => (int)$atts['columns']
791 + )
792 + )
793 + );
794 + $params = apply_filters( 'propertyhive_carousel_params', $params );
795 + $params = apply_filters( 'propertyhive_recent_properties_carousel_params', $params );
796 + wp_localize_script( 'propertyhive_carousel', 'propertyhive_carousel_params', $params );
797 +
798 + wp_enqueue_style( 'tiny_slider_css' );
799 + wp_enqueue_script( 'tiny_slider' );
800 + wp_enqueue_script( 'propertyhive_carousel' );
801 + }
802 +
452 803 $meta_query = PH()->query->get_meta_query();
453 804
454 805 if ( isset($atts['department']) && $atts['department'] != '' )
455 806 {
807 + $departments = explode(",", $atts['department']);
808 + $departments = array_map('trim', $departments);
809 + $departments = array_filter($departments);
810 +
456 811 $meta_query[] = array(
457 812 'key' => '_department',
458 - 'value' => $atts['department'],
459 - 'compare' => '='
813 + 'value' => $departments,
814 + 'compare' => 'IN'
460 815 );
461 816 }
462 817
818 + $base_department = $atts['department'];
819 + if ( $atts['department'] !== '' && !in_array($atts['department'], array_keys( ph_get_departments( true ) )) )
820 + {
821 + $base_department = ph_get_custom_department_based_on($base_department);
822 + }
823 +
824 + if ( isset($atts['department']) && $base_department == 'residential-sales' && isset($atts['minimum_price']) && $atts['minimum_price'] != '' )
825 + {
826 + $search_form_currency = get_option( 'propertyhive_search_form_currency', 'GBP' );
827 +
828 + $minimum_price = $atts['minimum_price'];
829 + if ( $search_form_currency != 'GBP' )
830 + {
831 + // Convert $atts['minimum_price'] to GBP
832 + $ph_countries = new PH_Countries();
833 +
834 + $minimum_price = $ph_countries->convert_price_to_gbp( $minimum_price, $search_form_currency );
835 + }
836 +
837 + $meta_query[] = array(
838 + 'key' => '_price_actual',
839 + 'value' => sanitize_text_field( floor( $minimum_price ) ),
840 + 'compare' => '>=',
841 + 'type' => 'NUMERIC'
842 + );
843 + }
844 +
463 845 if ( isset($atts['office_id']) && $atts['office_id'] != '' )
464 846 {
465 847 $meta_query[] = array(
466 848 'key' => '_office_id',
@@ -468,8 +850,35 @@
468 850 'compare' => 'IN'
469 851 );
470 852 }
471 853
854 + if ( isset($atts['negotiator_id']) && $atts['negotiator_id'] != '' )
855 + {
856 + $meta_query[] = array(
857 + 'key' => '_negotiator_id',
858 + 'value' => explode(",", $atts['negotiator_id']),
859 + 'compare' => 'IN',
860 + );
861 + }
862 +
863 + if ( isset($atts['commercial_for_sale']) && $atts['commercial_for_sale'] != '' )
864 + {
865 + $meta_query[] = array(
866 + 'key' => '_for_sale',
867 + 'value' => 'yes',
868 + 'compare' => '=',
869 + );
870 + }
871 +
872 + if ( isset($atts['commercial_to_rent']) && $atts['commercial_to_rent'] != '' )
873 + {
874 + $meta_query[] = array(
875 + 'key' => '_to_rent',
876 + 'value' => 'yes',
877 + 'compare' => '=',
878 + );
879 + }
880 +
472 881 $tax_query = array();
473 882
474 883 if ( isset($atts['availability_id']) && $atts['availability_id'] != '' )
475 884 {
@@ -479,28 +888,137 @@
479 888 'compare' => 'IN',
480 889 );
481 890 }
482 891
892 + if ( isset($atts['marketing_flag_id']) && $atts['marketing_flag_id'] != '' )
893 + {
894 + $tax_query[] = array(
895 + 'taxonomy' => 'marketing_flag',
896 + 'terms' => explode(",", $atts['marketing_flag_id']),
897 + 'compare' => 'IN',
898 + );
899 + }
900 +
901 + if ( isset($atts['property_type_id']) && $atts['property_type_id'] != '' )
902 + {
903 + // Change field to check when department is specified as commercial, or if commercial is the only active department
904 + if (
905 + ( isset($atts['department']) && $base_department == 'commercial' ) ||
906 + (
907 + !isset($atts['department']) &&
908 + get_option( 'propertyhive_active_departments_sales' ) != 'yes' &&
909 + get_option( 'propertyhive_active_departments_lettings' ) != 'yes' &&
910 + get_option( 'propertyhive_active_departments_commercial' ) == 'yes'
911 + )
912 + )
913 + {
914 + $tax_query[] = array(
915 + 'taxonomy' => 'commercial_property_type',
916 + 'terms' => explode(",", $atts['property_type_id']),
917 + 'compare' => 'IN',
918 + );
919 + }
920 + else
921 + {
922 + $tax_query[] = array(
923 + 'taxonomy' => 'property_type',
924 + 'terms' => explode(",", $atts['property_type_id']),
925 + 'compare' => 'IN',
926 + );
927 + }
928 + }
929 +
930 + if ( isset($atts['location_id']) && $atts['location_id'] != '' )
931 + {
932 + $tax_query[] = array(
933 + 'taxonomy' => 'location',
934 + 'terms' => explode(",", $atts['location_id']),
935 + 'compare' => 'IN',
936 + );
937 + }
938 +
939 + if ( isset($atts['sale_by_id']) && $atts['sale_by_id'] != '' )
940 + {
941 + $tax_query[] = array(
942 + 'taxonomy' => 'sale_by',
943 + 'terms' => explode(",", $atts['sale_by_id']),
944 + 'compare' => 'IN',
945 + );
946 + }
947 +
948 + // Get which page we're currently viewing from the URL
949 + $paged = max( 1, get_query_var( 'paged' ) );
950 +
483 951 $args = array(
484 952 'post_type' => 'property',
485 953 'post_status' => ( ( is_user_logged_in() && current_user_can( 'manage_propertyhive' ) ) ? array('publish', 'private') : 'publish' ),
486 954 'ignore_sticky_posts' => 1,
487 955 'posts_per_page' => $atts['per_page'],
956 + 'paged' => $paged,
488 957 'orderby' => $atts['orderby'],
489 958 'order' => $atts['order'],
959 + // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- Property eligibility and matching fields live in the established metadata schema; retain these filters and the shortcode page limit.
490 960 'meta_query' => $meta_query,
961 + // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query -- Property taxonomy filters are required by this shortcode; WordPress builds the query and the shortcode page limit is retained.
491 962 'tax_query' => $tax_query,
963 + 'has_password' => false,
492 964 );
493 965
966 + if ( ! empty( $atts['exclude'] ) )
967 + {
968 + $exclude_ids = array_map( 'absint', explode( ',', $atts['exclude'] ) );
969 + $exclude_ids = array_filter( $exclude_ids );
970 + if ( ! empty( $exclude_ids ) ) {
971 + // phpcs:ignore WordPressVIPMinimum.Performance.WPQueryParams.PostNotIn_post__not_in -- Explicit shortcode exclusions are validated as integer IDs above; retain this published selection feature within the shortcode page limit.
972 + $args['post__not_in'] = $exclude_ids;
973 + }
974 + }
975 +
976 + if ( isset($atts['orderby']) && $atts['orderby'] == 'date' )
977 + {
978 + $args['orderby'] = 'meta_value';
979 + // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- Property sorting uses the established price, floor-area or market-date metadata; retain the selected ordering and existing query limits.
980 + $args['meta_key'] = '_on_market_change_date';
981 + }
982 +
983 + $args['orderby'] .= ' post_title';
984 +
494 985 ob_start();
495 986
987 + if ( isset($atts['show_order']) && $atts['show_order'] != '' )
988 + {
989 + list( $args, $orderby ) = self::get_show_order_args( $atts, $args );
990 +
991 + propertyhive_catalog_ordering( $atts['department'], $orderby );
992 + }
993 +
496 994 $properties = new WP_Query( apply_filters( 'propertyhive_shortcode_recent_properties_query', $args, $atts ) );
497 995
498 - $propertyhive_loop['columns'] = $atts['columns'];
996 + if ( isset($atts['show_result_count']) && $atts['show_result_count'] != '' )
997 + {
998 + $total_posts = $properties->found_posts;
499 999
1000 + $first = ( $atts['per_page'] * $paged ) - $atts['per_page'] + 1;
1001 + $last = min( $total_posts, $atts['per_page'] * $paged );
1002 +
1003 + propertyhive_result_count( $paged, $atts['per_page'], $total_posts, $first, $last);
1004 + }
1005 +
1006 + $propertyhive_loop['columns'] = (int)$atts['columns'];
1007 +
500 1008 if ( $properties->have_posts() ) : ?>
501 1009
502 - <?php propertyhive_property_loop_start(); ?>
1010 + <?php
1011 + ob_start();
1012 + propertyhive_property_loop_start();
1013 + $loop_start = ob_get_clean();
1014 + if ( isset($atts['carousel']) && !empty($atts['carousel']) )
1015 + {
1016 + $loop_start = str_replace("class=\"properties", "class=\"properties propertyhive-shortcode-carousel", $loop_start);
1017 + }
1018 + // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Buffered loop template HTML; preserve theme overrides and the static carousel class insertion.
1019 + echo $loop_start;
1020 + ?>
503 1021
504 1022 <?php while ( $properties->have_posts() ) : $properties->the_post(); ?>
505 1023
506 1024 <?php ph_get_template_part( 'content', 'property-recent' ); ?>
@@ -510,17 +1028,22 @@
510 1028 <?php propertyhive_property_loop_end(); ?>
511 1029
512 1030 <?php else: ?>
513 1031
514 - <?php echo $atts['no_results_output']; ?>
1032 + <p class="propertyhive-info no-results-message"><?php echo wp_kses_post($atts['no_results_output']); ?></p>
515 1033
516 1034 <?php endif;
517 1035
1036 + if ( isset($atts['pagination']) && $atts['pagination'] != '' )
1037 + {
1038 + propertyhive_pagination( $properties->max_num_pages );
1039 + }
1040 +
518 1041 wp_reset_postdata();
519 1042
520 1043 $shortcode_output = ob_get_clean();
521 1044
522 - return apply_filters( 'propertyhive_recent_properties_shortcode_output', '<div class="propertyhive propertyhive-recent-properties-shortcode columns-' . $atts['columns'] . '">' . $shortcode_output . '</div>', $shortcode_output );
1045 + return apply_filters( 'propertyhive_recent_properties_shortcode_output', '<div class="propertyhive propertyhive-recent-properties-shortcode columns-' . (int)$atts['columns'] . '">' . $shortcode_output . '</div>', $shortcode_output );
523 1046
524 1047 }
525 1048
526 1049 /**
@@ -536,23 +1059,62 @@
536 1059 $atts = shortcode_atts( array(
537 1060 'per_page' => '12',
538 1061 'columns' => '4',
539 1062 'department' => '',
1063 + 'address_keyword' => '',
540 1064 'office_id' => '',
1065 + 'negotiator_id' => '',
541 1066 'availability_id' => '',
1067 + // phpcs:ignore WordPressVIPMinimum.Performance.WPQueryParams.PostNotIn_exclude -- These lines declare shortcode defaults; they do not pass a meta_key/exclude value to WP_Query or get_posts. 131 is the properties shortcode_atts default meta_key; 133/751/1045/1338 are exclude defaults in shortcode_atts.
1068 + 'exclude' => '',
542 1069 'orderby' => 'rand',
543 1070 'order' => 'desc',
1071 + // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- Shortcode default declaration only; no query executes here.
544 1072 'meta_key' => '',
545 1073 'no_results_output' => '',
1074 + 'pagination' => '',
1075 + 'show_order' => '',
1076 + 'show_result_count' => '',
1077 + 'carousel' => '',
546 1078 ), $atts, 'featured_properties' );
547 1079
1080 + if ( isset($atts['carousel']) && !empty($atts['carousel']) )
1081 + {
1082 + $params = array(
1083 + 'items' => 1,
1084 + 'controlsPosition' => 'bottom',
1085 + 'gutter' => 20,
1086 + 'mouseDrag' => true,
1087 + 'nav' => false,
1088 + 'navPosition' => 'bottom',
1089 + 'controlsText' => array("Prev", "Next"),
1090 + 'responsive' => array(
1091 + 640 => array(
1092 + 'items' => (int)$atts['columns']
1093 + )
1094 + )
1095 + );
1096 + $params = apply_filters( 'propertyhive_carousel_params', $params );
1097 + $params = apply_filters( 'propertyhive_featured_properties_carousel_params', $params );
1098 + wp_localize_script( 'propertyhive_carousel', 'propertyhive_carousel_params', $params );
1099 +
1100 + wp_enqueue_style( 'tiny_slider_css' );
1101 + wp_enqueue_script( 'tiny_slider' );
1102 + wp_enqueue_script( 'propertyhive_carousel' );
1103 + }
1104 +
1105 + // Get which page we're currently viewing from the URL
1106 + $paged = max( 1, get_query_var( 'paged' ) );
1107 +
548 1108 $args = array(
549 1109 'post_type' => 'property',
550 1110 'post_status' => ( ( is_user_logged_in() && current_user_can( 'manage_propertyhive' ) ) ? array('publish', 'private') : 'publish' ),
551 1111 'ignore_sticky_posts' => 1,
552 1112 'posts_per_page' => $atts['per_page'],
1113 + 'paged' => $paged,
553 1114 'orderby' => $atts['orderby'],
554 1115 'order' => $atts['order'],
1116 + 'has_password' => false,
555 1117 );
556 1118
557 1119 $meta_query = array(
558 1120 array(
@@ -566,15 +1128,98 @@
566 1128 );
567 1129
568 1130 if ( isset($atts['department']) && $atts['department'] != '' )
569 1131 {
1132 + $departments = explode(",", $atts['department']);
1133 + $departments = array_map('trim', $departments);
1134 + $departments = array_filter($departments);
1135 +
570 1136 $meta_query[] = array(
571 1137 'key' => '_department',
572 - 'value' => $atts['department'],
573 - 'compare' => '='
1138 + 'value' => $departments,
1139 + 'compare' => 'IN'
574 1140 );
575 1141 }
576 1142
1143 + if ( isset($atts['address_keyword']) && $atts['address_keyword'] != '' )
1144 + {
1145 + $atts['address_keyword'] = sanitize_text_field( trim( $atts['address_keyword'] ) );
1146 +
1147 + $address_keywords = array( $atts['address_keyword'] );
1148 +
1149 + if ( strpos( $atts['address_keyword'], ' ' ) !== FALSE )
1150 + {
1151 + $address_keywords[] = str_replace(" ", "-", $atts['address_keyword']);
1152 + }
1153 + if ( strpos( $atts['address_keyword'], '-' ) !== FALSE )
1154 + {
1155 + $address_keywords[] = str_replace("-", " ", $atts['address_keyword']);
1156 + }
1157 +
1158 + $sub_meta_query = array('relation' => 'OR');
1159 +
1160 + $address_keyword_compare = get_option( 'propertyhive_address_keyword_compare', '=' );
1161 + if ( $address_keyword_compare == 'polygon' )
1162 + {
1163 + $address_keyword_compare = apply_filters('propertyhive_shortcode_address_keyword_compare', '=');
1164 + }
1165 +
1166 + foreach ( $address_keywords as $address_keyword )
1167 + {
1168 + $sub_meta_query[] = array(
1169 + 'key' => '_reference_number',
1170 + 'value' => $address_keyword,
1171 + 'compare' => $address_keyword_compare
1172 + );
1173 + $sub_meta_query[] = array(
1174 + 'key' => '_address_street',
1175 + 'value' => $address_keyword,
1176 + 'compare' => $address_keyword_compare
1177 + );
1178 + $sub_meta_query[] = array(
1179 + 'key' => '_address_two',
1180 + 'value' => $address_keyword,
1181 + 'compare' => $address_keyword_compare
1182 + );
1183 + $sub_meta_query[] = array(
1184 + 'key' => '_address_three',
1185 + 'value' => $address_keyword,
1186 + 'compare' => $address_keyword_compare
1187 + );
1188 + $sub_meta_query[] = array(
1189 + 'key' => '_address_four',
1190 + 'value' => $address_keyword,
1191 + 'compare' => $address_keyword_compare
1192 + );
1193 + }
1194 + if ( strlen($atts['address_keyword']) <= 4 )
1195 + {
1196 + $sub_meta_query[] = array(
1197 + 'key' => '_address_postcode',
1198 + 'value' => sanitize_text_field( $atts['address_keyword'] ),
1199 + 'compare' => '='
1200 + );
1201 + // Run regex match where given keyword is at the start of the postcode ^
1202 + // followed by one or zero letters (for WC2E-style postcodes) [a-zA-Z]?
1203 + // then a single space [ ]
1204 + $sub_meta_query[] = array(
1205 + 'key' => '_address_postcode',
1206 + 'value' => sanitize_text_field( $atts['address_keyword'] ) . '[a-zA-Z]?[ ]',
1207 + 'compare' => 'RLIKE'
1208 + );
1209 + }
1210 + else
1211 + {
1212 + $sub_meta_query[] = array(
1213 + 'key' => '_address_postcode',
1214 + 'value' => sanitize_text_field( $atts['address_keyword'] ),
1215 + 'compare' => 'LIKE'
1216 + );
1217 + }
1218 +
1219 + $meta_query[] = $sub_meta_query;
1220 + }
1221 +
577 1222 if ( isset($atts['office_id']) && $atts['office_id'] != '' )
578 1223 {
579 1224 $meta_query[] = array(
580 1225 'key' => '_office_id',
@@ -582,11 +1227,22 @@
582 1227 'compare' => 'IN'
583 1228 );
584 1229 }
585 1230
1231 + if ( isset($atts['negotiator_id']) && $atts['negotiator_id'] != '' )
1232 + {
1233 + $meta_query[] = array(
1234 + 'key' => '_negotiator_id',
1235 + 'value' => explode(",", $atts['negotiator_id']),
1236 + 'compare' => 'IN',
1237 + );
1238 + }
1239 +
1240 + // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- Property eligibility and matching fields live in the established metadata schema; retain these filters and the shortcode page limit.
586 1241 $args['meta_query'] = $meta_query;
587 1242
588 1243 if ( ! empty( $atts['meta_key'] ) ) {
1244 + // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- Property sorting uses the established price, floor-area or market-date metadata; retain the selected ordering and existing query limits.
589 1245 $args['meta_key'] = $atts['meta_key'];
590 1246 }
591 1247
592 1248 $tax_query = array();
@@ -600,20 +1256,67 @@
600 1256 );
601 1257 }
602 1258
603 1259 if ( ! empty( $tax_query ) ) {
1260 + // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query -- Property taxonomy filters are required by this shortcode; WordPress builds the query and the shortcode page limit is retained.
604 1261 $args['tax_query'] = $tax_query;
605 1262 }
606 1263
1264 + if ( isset($atts['orderby']) && $atts['orderby'] == 'date' )
1265 + {
1266 + $args['orderby'] = 'meta_value';
1267 + // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- Property sorting uses the established price, floor-area or market-date metadata; retain the selected ordering and existing query limits.
1268 + $args['meta_key'] = '_on_market_change_date';
1269 + }
1270 +
1271 + $args['orderby'] .= ' post_title';
1272 +
1273 + if ( ! empty( $atts['exclude'] ) )
1274 + {
1275 + $exclude_ids = array_map( 'absint', explode( ',', $atts['exclude'] ) );
1276 + $exclude_ids = array_filter( $exclude_ids );
1277 + if ( ! empty( $exclude_ids ) ) {
1278 + // phpcs:ignore WordPressVIPMinimum.Performance.WPQueryParams.PostNotIn_post__not_in -- Explicit shortcode exclusions are validated as integer IDs above; retain this published selection feature within the shortcode page limit.
1279 + $args['post__not_in'] = $exclude_ids;
1280 + }
1281 + }
1282 +
607 1283 ob_start();
608 1284
1285 + if ( isset($atts['show_order']) && $atts['show_order'] != '' )
1286 + {
1287 + list( $args, $orderby ) = self::get_show_order_args( $atts, $args );
1288 +
1289 + propertyhive_catalog_ordering( $atts['department'], $orderby );
1290 + }
1291 +
609 1292 $properties = new WP_Query( apply_filters( 'propertyhive_shortcode_featured_properties_query', $args, $atts ) );
610 1293
611 - $propertyhive_loop['columns'] = $atts['columns'];
1294 + if ( isset($atts['show_result_count']) && $atts['show_result_count'] != '' )
1295 + {
1296 + $total_posts = $properties->found_posts;
612 1297
1298 + $first = ( $atts['per_page'] * $paged ) - $atts['per_page'] + 1;
1299 + $last = min( $total_posts, $atts['per_page'] * $paged );
1300 +
1301 + propertyhive_result_count( $paged, $atts['per_page'], $total_posts, $first, $last);
1302 + }
1303 +
1304 + $propertyhive_loop['columns'] = (int)$atts['columns'];
1305 +
613 1306 if ( $properties->have_posts() ) : ?>
614 1307
615 - <?php propertyhive_property_loop_start(); ?>
1308 + <?php
1309 + ob_start();
1310 + propertyhive_property_loop_start();
1311 + $loop_start = ob_get_clean();
1312 + if ( isset($atts['carousel']) && !empty($atts['carousel']) )
1313 + {
1314 + $loop_start = str_replace("class=\"properties", "class=\"properties propertyhive-shortcode-carousel", $loop_start);
1315 + }
1316 + // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Buffered loop template HTML; preserve theme overrides and the static carousel class insertion.
1317 + echo $loop_start;
1318 + ?>
616 1319
617 1320 <?php while ( $properties->have_posts() ) : $properties->the_post(); ?>
618 1321
619 1322 <?php ph_get_template_part( 'content', 'property-featured' ); ?>
@@ -623,17 +1326,22 @@
623 1326 <?php propertyhive_property_loop_end(); ?>
624 1327
625 1328 <?php else: ?>
626 1329
627 - <?php echo $atts['no_results_output']; ?>
1330 + <p class="propertyhive-info no-results-message"><?php echo wp_kses_post($atts['no_results_output']); ?></p>
628 1331
629 1332 <?php endif;
630 1333
1334 + if ( isset($atts['pagination']) && $atts['pagination'] != '' )
1335 + {
1336 + propertyhive_pagination( $properties->max_num_pages );
1337 + }
1338 +
631 1339 wp_reset_postdata();
632 1340
633 1341 $shortcode_output = ob_get_clean();
634 1342
635 - return apply_filters( 'propertyhive_featured_properties_shortcode_output', '<div class="propertyhive propertyhive-featured-properties-shortcode columns-' . $atts['columns'] . '">' . $shortcode_output . '</div>', $shortcode_output );
1343 + return apply_filters( 'propertyhive_featured_properties_shortcode_output', '<div class="propertyhive propertyhive-featured-properties-shortcode columns-' . (int)$atts['columns'] . '">' . $shortcode_output . '</div>', $shortcode_output );
636 1344 }
637 1345
638 1346 /**
639 1347 * Output similar properties
@@ -642,8 +1350,10 @@
642 1350 * @return string
643 1351 */
644 1352 public static function similar_properties( $atts ) {
645 1353
1354 + global $property, $propertyhive_loop;
1355 +
646 1356 $atts = shortcode_atts( array(
647 1357 'per_page' => '2',
648 1358 'columns' => '2',
649 1359 'orderby' => 'rand',
@@ -649,38 +1359,56 @@
649 1359 'orderby' => 'rand',
650 1360 'order' => 'asc',
651 1361 'price_percentage_bounds' => 10,
652 1362 'bedroom_bounds' => 0,
1363 + 'matching_address_field' => '', // only return fields with matching address field. Options: address_two, address_three, address_four, location
653 1364 'property_id' => '',
654 1365 'availability_id' => '',
1366 + 'property_type_id' => '',
1367 + 'match_property_type' => '',
1368 + // phpcs:ignore WordPressVIPMinimum.Performance.WPQueryParams.PostNotIn_exclude -- These lines declare shortcode defaults; they do not pass a meta_key/exclude value to WP_Query or get_posts. 131 is the properties shortcode_atts default meta_key; 133/751/1045/1338 are exclude defaults in shortcode_atts.
1369 + 'exclude' => '',
655 1370 'no_results_output' => '',
1371 + 'carousel' => '',
656 1372 ), $atts, 'similar_properties' );
657 1373
1374 + if ( isset($atts['carousel']) && !empty($atts['carousel']) )
1375 + {
1376 + $params = array(
1377 + 'items' => 1,
1378 + 'controlsPosition' => 'bottom',
1379 + 'gutter' => 20,
1380 + 'mouseDrag' => true,
1381 + 'nav' => false,
1382 + 'navPosition' => 'bottom',
1383 + 'controlsText' => array("Prev", "Next"),
1384 + 'responsive' => array(
1385 + 640 => array(
1386 + 'items' => (int)$atts['columns']
1387 + )
1388 + )
1389 + );
1390 + $params = apply_filters( 'propertyhive_carousel_params', $params );
1391 + $params = apply_filters( 'propertyhive_similar_properties_carousel_params', $params );
1392 + wp_localize_script( 'propertyhive_carousel', 'propertyhive_carousel_params', $params );
1393 +
1394 + wp_enqueue_style( 'tiny_slider_css' );
1395 + wp_enqueue_script( 'tiny_slider' );
1396 + wp_enqueue_script( 'propertyhive_carousel' );
1397 + }
1398 +
1399 + if ( $atts['property_id'] == '' && isset($property->id) )
1400 + {
1401 + $atts['property_id'] = $property->id;
1402 + }
1403 +
658 1404 if ($atts['property_id'] != '')
659 1405 {
660 1406 $department = get_post_meta( $atts['property_id'], '_department', true );
661 1407
662 - $price = get_post_meta( $atts['property_id'], '_price_actual', true );
663 - $lower_price = $price;
664 - $higher_price = $price;
665 - $atts['price_percentage_bounds'] = str_replace("%", "", $atts['price_percentage_bounds']);
666 - if ( isset($atts['price_percentage_bounds']) && $atts['price_percentage_bounds'] != '' && is_numeric($atts['price_percentage_bounds']) && $atts['price_percentage_bounds'] > 0 )
667 - {
668 - $lower_price = $price - ($price * $atts['price_percentage_bounds'] / 100);
669 - $higher_price = $price + ($price * $atts['price_percentage_bounds'] / 100);
670 - }
671 -
672 - $bedrooms = get_post_meta( $atts['property_id'], '_bedrooms', true );
673 - $lower_bedrooms = $bedrooms;
674 - $higher_bedrooms = $bedrooms;
675 - if ( isset($atts['bedroom_bounds']) && $atts['bedroom_bounds'] != '' && is_numeric($atts['bedroom_bounds']) && $atts['bedroom_bounds'] > 0 )
676 - {
677 - $lower_bedrooms = $bedrooms - $atts['bedroom_bounds'];
678 - $higher_bedrooms = $bedrooms + $atts['bedroom_bounds'];
679 - }
680 -
681 1408 $args = array(
682 1409 'post_type' => 'property',
1410 + // phpcs:ignore WordPressVIPMinimum.Performance.WPQueryParams.PostNotIn_post__not_in -- Similar-property results must exclude the one current property; the result count is limited by per_page.
683 1411 'post__not_in' => array($atts['property_id']),
684 1412 'post_status' => ( ( is_user_logged_in() && current_user_can( 'manage_propertyhive' ) ) ? array('publish', 'private') : 'publish' ),
685 1413 'ignore_sticky_posts' => 1,
686 1414 'posts_per_page' => $atts['per_page'],
@@ -685,8 +1413,9 @@
685 1413 'ignore_sticky_posts' => 1,
686 1414 'posts_per_page' => $atts['per_page'],
687 1415 'orderby' => $atts['orderby'],
688 1416 'order' => $atts['order'],
1417 + 'has_password' => false,
689 1418 );
690 1419
691 1420 $meta_query = array();
692 1421
@@ -699,42 +1428,218 @@
699 1428 'key' => '_on_market',
700 1429 'value' => 'yes',
701 1430 );
702 1431
703 - if ( isset($atts['bedroom_bounds']) && is_numeric($atts['bedroom_bounds']) )
1432 + if ( $department != 'commercial' && ph_get_custom_department_based_on( $department ) != 'commercial' )
704 1433 {
705 - $meta_query[] = array(
706 - 'key' => '_bedrooms',
707 - 'value' => $lower_bedrooms,
708 - 'compare' => '>=',
709 - 'type' => 'NUMERIC'
710 - );
1434 + // residential
1435 + $bedrooms = get_post_meta( $atts['property_id'], '_bedrooms', true );
1436 + $lower_bedrooms = $bedrooms;
1437 + $higher_bedrooms = $bedrooms;
1438 + if ( !empty($bedrooms) && isset($atts['bedroom_bounds']) && $atts['bedroom_bounds'] != '' && is_numeric($atts['bedroom_bounds']) && $atts['bedroom_bounds'] > 0 )
1439 + {
1440 + $lower_bedrooms = $bedrooms - (int)$atts['bedroom_bounds'];
1441 + $higher_bedrooms = $bedrooms + (int)$atts['bedroom_bounds'];
1442 + }
711 1443
712 - $meta_query[] = array(
713 - 'key' => '_bedrooms',
714 - 'value' => $higher_bedrooms,
715 - 'compare' => '<=',
716 - 'type' => 'NUMERIC'
717 - );
1444 + if ( isset($atts['bedroom_bounds']) && is_numeric($atts['bedroom_bounds']) )
1445 + {
1446 + $meta_query[] = array(
1447 + 'key' => '_bedrooms',
1448 + 'value' => array( $lower_bedrooms, $higher_bedrooms ),
1449 + 'compare' => 'BETWEEN',
1450 + 'type' => 'NUMERIC'
1451 + );
1452 + }
1453 +
1454 + $price = get_post_meta( $atts['property_id'], '_price_actual', true );
1455 + $lower_price = $price;
1456 + $higher_price = $price;
1457 + $atts['price_percentage_bounds'] = str_replace("%", "", $atts['price_percentage_bounds']);
1458 + if ( !empty($price) && isset($atts['price_percentage_bounds']) && $atts['price_percentage_bounds'] != '' && is_numeric($atts['price_percentage_bounds']) && $atts['price_percentage_bounds'] > 0 )
1459 + {
1460 + $lower_price = $price - ($price * (int)$atts['price_percentage_bounds'] / 100);
1461 + $higher_price = $price + ($price * (int)$atts['price_percentage_bounds'] / 100);
1462 + }
1463 +
1464 + if ( isset($atts['price_percentage_bounds']) && is_numeric($atts['price_percentage_bounds']) )
1465 + {
1466 + $meta_query[] = array(
1467 + 'key' => '_price_actual',
1468 + 'value' => array( $lower_price, $higher_price ),
1469 + 'compare' => 'BETWEEN',
1470 + 'type' => 'NUMERIC'
1471 + );
1472 + }
718 1473 }
1474 + else
1475 + {
1476 + // commercial
1477 + $for_sale = get_post_meta( $atts['property_id'], '_for_sale', true );
1478 + $to_rent = get_post_meta( $atts['property_id'], '_to_rent', true );
719 1479
720 - if ( isset($atts['price_percentage_bounds']) && is_numeric($atts['price_percentage_bounds']) )
1480 + if ( $for_sale == 'yes' || $to_rent == 'yes' )
1481 + {
1482 + $sub_meta_query = array('relation' => 'OR');
1483 +
1484 + if ( $for_sale == 'yes' )
1485 + {
1486 + $prices_sub_query = array('relation' => 'OR');
1487 +
1488 + $price_from = get_post_meta( $atts['property_id'], '_price_from_actual', true );
1489 + $price_to = get_post_meta( $atts['property_id'], '_price_to_actual', true );
1490 +
1491 + if ( !empty($price_from) || !empty($price_to) )
1492 + {
1493 + if ( empty($price_from) )
1494 + {
1495 + $price_from = $price_to;
1496 + }
1497 + if ( empty($price_to) )
1498 + {
1499 + $price_to = $price_from;
1500 + }
1501 +
1502 + $lower_price_from = $price_from;
1503 + $higher_price_from = $price_from;
1504 + if ( isset($atts['price_percentage_bounds']) && $atts['price_percentage_bounds'] != '' && is_numeric($atts['price_percentage_bounds']) && $atts['price_percentage_bounds'] > 0 )
1505 + {
1506 + $lower_price_from = $price_from - ($price_from * (int)$atts['price_percentage_bounds'] / 100);
1507 + $higher_price_from = $price_from + ($price_from * (int)$atts['price_percentage_bounds'] / 100);
1508 + }
1509 +
1510 + $lower_price_to = $price_to;
1511 + $higher_price_to = $price_to;
1512 + if ( isset($atts['price_percentage_bounds']) && $atts['price_percentage_bounds'] != '' && is_numeric($atts['price_percentage_bounds']) && $atts['price_percentage_bounds'] > 0 )
1513 + {
1514 + $lower_price_to = $price_to - ($price_to * (int)$atts['price_percentage_bounds'] / 100);
1515 + $higher_price_to = $price_to + ($price_to * (int)$atts['price_percentage_bounds'] / 100);
1516 + }
1517 +
1518 + // where price from and price to not blank and price from -15%
1519 + $price_sub_query = array();
1520 +
1521 + $price_sub_query[] = array(
1522 + 'key' => '_price_from_actual',
1523 + 'value' => array( '', 0 ),
1524 + 'compare' => 'NOT IN'
1525 + );
1526 + $price_sub_query[] = array(
1527 + 'key' => '_price_to_actual',
1528 + 'value' => array( '', 0 ),
1529 + 'compare' => 'NOT IN'
1530 + );
1531 + $price_sub_query[] = array(
1532 + 'key' => '_price_to_actual',
1533 + 'value' => $lower_price_from,
1534 + 'compare' => '>=',
1535 + 'type' => 'NUMERIC'
1536 + );
1537 + $price_sub_query[] = array(
1538 + 'key' => '_price_from_actual',
1539 + 'value' => $higher_price_to,
1540 + 'compare' => '<=',
1541 + 'type' => 'NUMERIC'
1542 + );
1543 +
1544 + $prices_sub_query[] = $price_sub_query;
1545 + }
1546 +
1547 + $sub_meta_query[] = array(
1548 + array(
1549 + 'key' => '_for_sale',
1550 + 'value' => $for_sale,
1551 + ),
1552 + $prices_sub_query
1553 + );
1554 + }
1555 + elseif ( $to_rent == 'yes' )
1556 + {
1557 + $prices_sub_query = array('relation' => 'OR');
1558 +
1559 + $price_from = get_post_meta( $atts['property_id'], '_rent_from_actual', true );
1560 + $price_to = get_post_meta( $atts['property_id'], '_rent_to_actual', true );
1561 +
1562 + if ( !empty($price_from) || !empty($price_to) )
1563 + {
1564 + if ( empty($price_from) )
1565 + {
1566 + $price_from = $price_to;
1567 + }
1568 + if ( empty($price_to) )
1569 + {
1570 + $price_to = $price_from;
1571 + }
1572 +
1573 + $lower_price_from = $price_from;
1574 + $higher_price_from = $price_from;
1575 + if ( isset($atts['price_percentage_bounds']) && $atts['price_percentage_bounds'] != '' && is_numeric($atts['price_percentage_bounds']) && $atts['price_percentage_bounds'] > 0 )
1576 + {
1577 + $lower_price_from = $price_from - ($price_from * (int)$atts['price_percentage_bounds'] / 100);
1578 + $higher_price_from = $price_from + ($price_from * (int)$atts['price_percentage_bounds'] / 100);
1579 + }
1580 +
1581 + $lower_price_to = $price_to;
1582 + $higher_price_to = $price_to;
1583 + if ( isset($atts['price_percentage_bounds']) && $atts['price_percentage_bounds'] != '' && is_numeric($atts['price_percentage_bounds']) && $atts['price_percentage_bounds'] > 0 )
1584 + {
1585 + $lower_price_to = $price_to - ($price_to * (int)$atts['price_percentage_bounds'] / 100);
1586 + $higher_price_to = $price_to + ($price_to * (int)$atts['price_percentage_bounds'] / 100);
1587 + }
1588 +
1589 + // where price from and price to not blank and price from -15%
1590 + $price_sub_query = array();
1591 +
1592 + $price_sub_query[] = array(
1593 + 'key' => '_rent_from_actual',
1594 + 'value' => array( '', 0 ),
1595 + 'compare' => 'NOT IN'
1596 + );
1597 + $price_sub_query[] = array(
1598 + 'key' => '_rent_to_actual',
1599 + 'value' => array( '', 0 ),
1600 + 'compare' => 'NOT IN'
1601 + );
1602 + $price_sub_query[] = array(
1603 + 'key' => '_rent_to_actual',
1604 + 'value' => $lower_price_from,
1605 + 'compare' => '>=',
1606 + 'type' => 'NUMERIC'
1607 + );
1608 + $price_sub_query[] = array(
1609 + 'key' => '_rent_from_actual',
1610 + 'value' => $higher_price_to,
1611 + 'compare' => '<=',
1612 + 'type' => 'NUMERIC'
1613 + );
1614 +
1615 + $prices_sub_query[] = $price_sub_query;
1616 + }
1617 +
1618 + $sub_meta_query[] = array(
1619 + array(
1620 + 'key' => '_to_rent',
1621 + 'value' => $to_rent,
1622 + ),
1623 + $prices_sub_query
1624 + );
1625 + }
1626 +
1627 + $meta_query[] = $sub_meta_query;
1628 + }
1629 + }
1630 +
1631 + if ( isset($atts['matching_address_field']) && in_array($atts['matching_address_field'], array( 'address_two', 'address_three', 'address_four' )) )
721 1632 {
722 - $meta_query[] = array(
723 - 'key' => '_price_actual',
724 - 'value' => $lower_price,
725 - 'compare' => '>=',
726 - 'type' => 'NUMERIC'
727 - );
1633 + $address_field = get_post_meta( $atts['property_id'], '_' . $atts['matching_address_field'], true );
728 1634
729 1635 $meta_query[] = array(
730 - 'key' => '_price_actual',
731 - 'value' => $higher_price,
732 - 'compare' => '<=',
733 - 'type' => 'NUMERIC'
1636 + 'key' => '_' . $atts['matching_address_field'],
1637 + 'value' => $address_field,
734 1638 );
735 1639 }
736 1640
1641 + // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- Property eligibility and matching fields live in the established metadata schema; retain these filters and the shortcode page limit.
737 1642 $args['meta_query'] = $meta_query;
738 1643
739 1644 $tax_query = array();
740 1645
@@ -746,21 +1651,104 @@
746 1651 'compare' => 'IN',
747 1652 );
748 1653 }
749 1654
1655 + $property_types = array();
1656 + if ( isset($atts['property_type_id']) && $atts['property_type_id'] != '' )
1657 + {
1658 + $property_types = explode(",", $atts['property_type_id']);
1659 + }
1660 +
1661 + if ( isset($atts['match_property_type']) && $atts['match_property_type'] != '' )
1662 + {
1663 + $term_list = wp_get_post_terms((int)$atts['property_id'], ( ( $department == 'commercial' || ph_get_custom_department_based_on( $department ) == 'commercial' ) ? 'commercial_' : '' ) . 'property_type', array("fields" => "ids"));
1664 +
1665 + if ( !is_wp_error($term_list) && is_array($term_list) && !empty($term_list) )
1666 + {
1667 + $property_types = $term_list;
1668 + }
1669 + }
1670 +
1671 + if ( !empty($property_types) )
1672 + {
1673 + $property_types = array_unique($property_types);
1674 + $property_types = array_filter($property_types);
1675 +
1676 + if ( $department != 'commercial' && ph_get_custom_department_based_on( $department ) != 'commercial' )
1677 + {
1678 + $tax_query[] = array(
1679 + 'taxonomy' => 'property_type',
1680 + 'terms' => $property_types,
1681 + 'compare' => 'IN',
1682 + );
1683 + }
1684 + else
1685 + {
1686 + $tax_query[] = array(
1687 + 'taxonomy' => 'commercial_property_type',
1688 + 'terms' => $property_types,
1689 + 'compare' => 'IN',
1690 + );
1691 + }
1692 + }
1693 +
1694 + if ( isset($atts['matching_address_field']) && $atts['matching_address_field'] == 'location' )
1695 + {
1696 + $term_list = wp_get_post_terms($atts['property_id'], 'location', array("fields" => "ids"));
1697 +
1698 + if ( !is_wp_error($term_list) && is_array($term_list) && !empty($term_list) )
1699 + {
1700 + $tax_query[] = array(
1701 + 'taxonomy' => 'location',
1702 + 'terms' => $term_list,
1703 + 'compare' => 'IN',
1704 + );
1705 + }
1706 + }
1707 +
750 1708 if ( ! empty( $tax_query ) ) {
1709 + // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query -- Property taxonomy filters are required by this shortcode; WordPress builds the query and the shortcode page limit is retained.
751 1710 $args['tax_query'] = $tax_query;
752 1711 }
753 1712
1713 + if ( isset($atts['orderby']) && $atts['orderby'] == 'date' )
1714 + {
1715 + $args['orderby'] = 'meta_value';
1716 + // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- Property sorting uses the established price, floor-area or market-date metadata; retain the selected ordering and existing query limits.
1717 + $args['meta_key'] = '_on_market_change_date';
1718 + }
1719 +
1720 + $args['orderby'] .= ' post_title';
1721 +
1722 + if ( ! empty( $atts['exclude'] ) )
1723 + {
1724 + $exclude_ids = array_map( 'absint', explode( ',', $atts['exclude'] ) );
1725 + $exclude_ids = array_filter( $exclude_ids );
1726 + if ( ! empty( $exclude_ids ) ) {
1727 + // phpcs:ignore WordPressVIPMinimum.Performance.WPQueryParams.PostNotIn_post__not_in -- Explicit shortcode exclusions are validated as integer IDs above; retain this published selection feature within the shortcode page limit.
1728 + $args['post__not_in'] = $exclude_ids;
1729 + }
1730 + }
1731 +
754 1732 ob_start();
755 1733
756 1734 $properties = new WP_Query( apply_filters( 'propertyhive_shortcode_similar_properties_query', $args, $atts ) );
757 1735
758 - $propertyhive_loop['columns'] = $atts['columns'];
1736 + $propertyhive_loop['columns'] = (int)$atts['columns'];
759 1737
760 1738 if ( $properties->have_posts() ) : ?>
761 1739
762 - <?php propertyhive_property_loop_start(); ?>
1740 + <?php
1741 + ob_start();
1742 + propertyhive_property_loop_start();
1743 + $loop_start = ob_get_clean();
1744 + if ( isset($atts['carousel']) && !empty($atts['carousel']) )
1745 + {
1746 + $loop_start = str_replace("class=\"properties", "class=\"properties propertyhive-shortcode-carousel", $loop_start);
1747 + }
1748 + // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Buffered loop template HTML; preserve theme overrides and the static carousel class insertion.
1749 + echo $loop_start;
1750 + ?>
763 1751
764 1752 <?php while ( $properties->have_posts() ) : $properties->the_post(); ?>
765 1753
766 1754 <?php ph_get_template_part( 'content', 'property-featured' ); ?>
@@ -770,9 +1758,9 @@
770 1758 <?php propertyhive_property_loop_end(); ?>
771 1759
772 1760 <?php else: ?>
773 1761
774 - <?php echo $atts['no_results_output']; ?>
1762 + <p class="propertyhive-info no-results-message"><?php echo wp_kses_post($atts['no_results_output']); ?></p>
775 1763
776 1764 <?php endif;
777 1765
778 1766 wp_reset_postdata();
@@ -783,9 +1771,9 @@
783 1771 }
784 1772
785 1773 $shortcode_output = ob_get_clean();
786 1774
787 - return apply_filters( 'propertyhive_similar_properties_shortcode_output', '<div class="propertyhive propertyhive-similar-properties-shortcode columns-' . $atts['columns'] . '">' . $shortcode_output . '</div>', $shortcode_output );
1775 + return apply_filters( 'propertyhive_similar_properties_shortcode_output', '<div class="propertyhive propertyhive-similar-properties-shortcode columns-' . (int)$atts['columns'] . '">' . $shortcode_output . '</div>', $shortcode_output );
788 1776 }
789 1777
790 1778 /**
791 1779 * Output property map
@@ -801,19 +1789,46 @@
801 1789 $atts = shortcode_atts( array(
802 1790 'id' => '',
803 1791 'height' => '400',
804 1792 'zoom' => '14',
805 - 'scrollwheel' => 'true'
1793 + 'scrollwheel' => 'true',
1794 + 'init_on_load' => 'true',
1795 + 'embed' => 'false'
806 1796 ), $atts, 'property_map' );
807 1797
808 1798 ob_start();
809 1799
810 - echo get_property_map( $atts );
1800 + get_property_map( $atts );
811 1801
812 1802 return ob_get_clean();
813 1803 }
814 1804
815 1805 /**
1806 + * Output static (image) property map
1807 + * Should only be used on a property page or where the $property var is set
1808 + *
1809 + * @param array $atts
1810 + * @return string
1811 + */
1812 + public static function property_static_map( $atts ) {
1813 +
1814 + global $property;
1815 +
1816 + $atts = shortcode_atts( array(
1817 + 'id' => '',
1818 + 'height' => '400',
1819 + 'zoom' => '14',
1820 + 'link' => 'true',
1821 + ), $atts, 'property_static_map' );
1822 +
1823 + ob_start();
1824 +
1825 + get_property_static_map( $atts );
1826 +
1827 + return ob_get_clean();
1828 + }
1829 +
1830 + /**
816 1831 * Output property street view
817 1832 * Should only be used on a property page or where the $property var is set
818 1833 *
819 1834 * @param array $atts
@@ -824,13 +1839,15 @@
824 1839 global $property;
825 1840
826 1841 $atts = shortcode_atts( array(
827 1842 'height' => '400',
1843 + 'init_on_load' => 'true',
1844 + 'embed' => 'false'
828 1845 ), $atts, 'property_street_view' );
829 1846
830 1847 ob_start();
831 1848
832 - echo get_property_street_view( $atts );
1849 + get_property_street_view( $atts );
833 1850
834 1851 return ob_get_clean();
835 1852 }
836 1853
@@ -861,24 +1878,24 @@
861 1878 echo '<div class="property-office-details">';
862 1879
863 1880 if ( $property->office_name != '' )
864 1881 {
865 - echo '<div class="office-name">' . $property->office_name . '</div>';
1882 + echo '<div class="office-name">' . esc_html($property->office_name) . '</div>';
866 1883 }
867 1884
868 1885 if ( $property->get_office_address( $atts['address_separator'] ) != '' )
869 1886 {
870 - echo '<div class="office-address">' . $property->get_office_address( $atts['address_separator'] ) . '</div>';
1887 + echo '<div class="office-address">' . wp_kses_post( $property->get_office_address( $atts['address_separator'] ) ) . '</div>';
871 1888 }
872 1889
873 1890 if ( $property->office_telephone_number != '' )
874 1891 {
875 - echo '<div class="office-telephone-number">' . ( ($atts['hyperlink_telephone_number'] === true) ? '<a href="tel:' . $property->office_telephone_number . '">' : '' ) . $property->office_telephone_number . ( ($atts['hyperlink_telephone_number'] === true) ? '</a>' : '' ) . '</div>';
1892 + echo '<div class="office-telephone-number">' . ( ($atts['hyperlink_telephone_number'] === true) ? '<a href="tel:' . esc_attr($property->office_telephone_number) . '">' : '' ) . esc_html($property->office_telephone_number) . ( ($atts['hyperlink_telephone_number'] === true) ? '</a>' : '' ) . '</div>';
876 1893 }
877 1894
878 1895 if ( $property->office_email_address != '' )
879 1896 {
880 - echo '<div class="office-email-address">' . ( ($atts['hyperlink_email_address'] === true) ? '<a href="mailto:' . $property->office_email_address . '">' : '' ) . $property->office_email_address . ( ($atts['hyperlink_email_address'] === true) ? '</a>' : '' ) . '</div>';
1897 + echo '<div class="office-email-address">' . ( ($atts['hyperlink_email_address'] === true) ? '<a href="mailto:' . esc_attr($property->office_email_address) . '">' : '' ) . esc_html($property->office_email_address) . ( ($atts['hyperlink_email_address'] === true) ? '</a>' : '' ) . '</div>';
881 1898 }
882 1899
883 1900 echo '</div>';
884 1901 }
@@ -931,12 +1948,12 @@
931 1948
932 1949 ob_start();
933 1950
934 1951 $api_key = get_option('propertyhive_google_maps_api_key', '');
935 - wp_register_script('googlemaps', '//maps.googleapis.com/maps/api/js?' . ( ( $api_key != '' && $api_key !== FALSE ) ? 'key=' . $api_key : '' ), false, '3');
1952 + wp_register_script('googlemaps', '//maps.googleapis.com/maps/api/js?' . ( ( $api_key != '' && $api_key !== FALSE ) ? 'key=' . $api_key : '' ), false, '3', true );
936 1953 wp_enqueue_script('googlemaps');
937 1954
938 - echo '<div id="office_map_canvas" style="height:' . str_replace( "px", "", ( ( isset($atts['height']) && !empty($atts['height']) ) ? $atts['height'] : '400' ) ) . 'px"></div>';
1955 + echo '<div id="office_map_canvas" style="height:' . (int) ( ( isset($atts['height']) && !empty($atts['height']) && is_numeric($atts['height']) ) ? $atts['height'] : 400 ) . 'px"></div>';
939 1956 ?>
940 1957 <script>
941 1958
942 1959 // We declare vars globally so developers can access them
@@ -957,8 +1974,9 @@
957 1974 $args['p'] = (int)$atts['office_id'];
958 1975 }
959 1976 else
960 1977 {
1978 + // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- Selects the primary office from the established metadata schema and returns at most one office.
961 1979 $args['meta_query'] = array(
962 1980 array(
963 1981 'key' => 'primary',
964 1982 'value' => '1'
@@ -986,11 +2004,11 @@
986 2004 $lat = '51.509865';
987 2005 $lng = '-0.118092';
988 2006 }
989 2007 ?>
990 - var myLatlng = new google.maps.LatLng(<?php echo $lat; ?>, <?php echo $lng; ?>);
2008 + var myLatlng = new google.maps.LatLng(<?php echo (float)$lat; ?>, <?php echo (float)$lng; ?>);
991 2009 var map_options = {
992 - zoom: <?php echo ( ( isset($atts['zoom']) && !empty($atts['zoom']) && $atts['zoom'] != 'auto' ) ? $atts['zoom'] : '14' ); ?>,
2010 + zoom: <?php echo ( ( isset($atts['zoom']) && !empty($atts['zoom']) && is_numeric($atts['zoom']) && $atts['zoom'] != 'auto' ) ? (int)$atts['zoom'] : '14' ); ?>,
993 2011 center: myLatlng,
994 2012 mapTypeId: google.maps.MapTypeId.ROADMAP,
995 2013 scrollwheel: <?php echo ( ( isset($atts['scrollwheel']) && ($atts['scrollwheel'] === 'false' || $atts['scrollwheel'] === FALSE) ) ? 'false' : 'true' ); ?>
996 2014 }
@@ -998,11 +2016,15 @@
998 2016 if ( class_exists( 'PH_Map_Search' ) )
999 2017 {
1000 2018 $map_add_on_settings = get_option( 'propertyhive_map_search', array() );
1001 2019
1002 - if ( isset($map_add_on_settings['style_js']) && trim($map_add_on_settings['style_js']) != '' )
2020 + if ( isset($map_add_on_settings['style_js']) && is_string($map_add_on_settings['style_js']) && trim($map_add_on_settings['style_js']) != '' )
1003 2021 {
1004 - echo 'map_options.styles = ' . trim($map_add_on_settings['style_js']) . ';';
2022 + // Google Maps styles are JSON arrays, including legacy Snazzy Maps style properties.
2023 + $map_styles = json_decode( $map_add_on_settings['style_js'] );
2024 + if ( is_array( $map_styles ) ) {
2025 + echo 'map_options.styles = ' . wp_json_encode( $map_styles, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT ) . ';';
2026 + }
1005 2027 }
1006 2028 }
1007 2029
1008 2030 do_action( 'propertyhive_office_map_options' );
@@ -1033,9 +2055,9 @@
1033 2055
1034 2056 if ( $lat != '' && $lng != '' )
1035 2057 {
1036 2058 ?>
1037 - var myLatlng = new google.maps.LatLng(<?php echo $lat; ?>, <?php echo $lng; ?>);
2059 + var myLatlng = new google.maps.LatLng(<?php echo (float)$lat; ?>, <?php echo (float)$lng; ?>);
1038 2060
1039 2061 var marker_options = {
1040 2062 map: office_map,
1041 2063 position: myLatlng,
@@ -1051,9 +2073,9 @@
1051 2073 {
1052 2074 $marker_icon_url = wp_get_attachment_url( $map_add_on_settings['custom_icon_attachment_id'] );
1053 2075 if ( $marker_icon_url !== FALSE )
1054 2076 {
1055 - echo 'marker_options.icon = \'' . $marker_icon_url . '\';';
2077 + echo 'marker_options.icon = \'' . esc_url($marker_icon_url) . '\';';
1056 2078 }
1057 2079 }
1058 2080 }
1059 2081 ?>
@@ -1124,9 +2146,9 @@
1124 2146 $form_controls = apply_filters( 'propertyhive_user_details_form_fields', $form_controls );
1125 2147
1126 2148 $form_controls_2 = ph_get_applicant_requirements_form_fields();
1127 2149
1128 - $form_controls_2 = apply_filters( 'propertyhive_applicant_requirements_form_fields', $form_controls_2 );
2150 + $form_controls_2 = apply_filters( 'propertyhive_applicant_requirements_form_fields', $form_controls_2, false );
1129 2151
1130 2152 $form_controls = array_merge( $form_controls, $form_controls_2 );
1131 2153
1132 2154 if ( get_option( 'propertyhive_applicant_registration_form_disclaimer', '' ) != '' )
@@ -1184,8 +2206,67 @@
1184 2206
1185 2207 }
1186 2208
1187 2209 /**
2210 + * Output 'Reset Password' page
2211 + *
2212 + * @param array $atts
2213 + * @return string
2214 + */
2215 + public static function reset_password_form( $atts )
2216 + {
2217 + $atts = shortcode_atts( array(
2218 +
2219 + ), $atts, 'reset_password_form' );
2220 +
2221 + $assets_path = str_replace( array( 'http:', 'https:' ), '', PH()->plugin_url() ) . '/assets/';
2222 + wp_enqueue_script( 'propertyhive_account', $assets_path . 'js/frontend/account.js', array( 'jquery' ), PH_VERSION, true );
2223 +
2224 + ob_start();
2225 +
2226 + if ( is_user_logged_in() )
2227 + {
2228 + ph_get_template( 'account/already-logged-in.php' );
2229 + return ob_get_clean();
2230 + }
2231 +
2232 + // Check 'propertyhive_applicant_users' setting is enabled
2233 + if ( get_option( 'propertyhive_applicant_users', '' ) != 'yes' )
2234 + {
2235 + ph_get_template( 'account/invalid-access.php' );
2236 + return ob_get_clean();
2237 + }
2238 +
2239 + // Display only: WordPress validates the opaque reset key; the reset action has its own nonce.
2240 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- An opaque key must remain intact for check_password_reset_key().
2241 + $key = isset( $_GET['key'] ) && is_string( $_GET['key'] ) ? wp_unslash( $_GET['key'] ) : '';
2242 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Only selects the user whose reset key WordPress verifies below.
2243 + $user_id = isset( $_GET['id'] ) && is_string( $_GET['id'] ) ? absint( $_GET['id'] ) : 0;
2244 + if ( $key === '' || $user_id === 0 )
2245 + {
2246 + echo esc_html(__( 'Invalid key or id provided. Please try again', 'propertyhive' ));
2247 + return ob_get_clean();
2248 + }
2249 +
2250 +
2251 + $userdata = get_userdata( $user_id );
2252 + $user_login = $userdata ? $userdata->user_login : '';
2253 +
2254 + $user = check_password_reset_key( $key, $user_login );
2255 +
2256 + if ( is_wp_error( $user ) )
2257 + {
2258 + echo esc_html(__( 'This key is invalid or has already been used. Please reset your password again if needed.', 'propertyhive' ));
2259 + return ob_get_clean();
2260 + }
2261 +
2262 + ph_get_template( 'account/reset-password-form.php', array( 'reset_key' => $key, 'reset_login' => $user_login ) );
2263 +
2264 + return ob_get_clean();
2265 +
2266 + }
2267 +
2268 + /**
1188 2269 * Output 'My Account' page
1189 2270 *
1190 2271 * @param array $atts
1191 2272 * @return string
@@ -1218,6 +2299,59 @@
1218 2299 ph_get_template( 'account/my-account.php' );
1219 2300
1220 2301 return ob_get_clean();
1221 2302
2303 + }
2304 +
2305 + private static function get_show_order_args( $atts, $args )
2306 + {
2307 + $orderby = '';
2308 +
2309 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Public read-only sort selection; the query helper validates the ordering value.
2310 + if ( isset( $_GET['orderby'] ) && is_string( $_GET['orderby'] ) && $_GET['orderby'] != '' )
2311 + {
2312 + $PH_Query = new PH_Query();
2313 + $ordering_args = $PH_Query->get_search_results_ordering_args();
2314 +
2315 + $args['orderby'] = $ordering_args['orderby'];
2316 + $args['order'] = $ordering_args['order'];
2317 +
2318 + if ( isset( $ordering_args['meta_key'] ) )
2319 + {
2320 + // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- Property sorting uses the established price, floor-area or market-date metadata; retain the selected ordering and existing query limits.
2321 + $args['meta_key'] = $ordering_args['meta_key'];
2322 + }
2323 + else
2324 + {
2325 + unset($args['meta_key']);
2326 + }
2327 + }
2328 + else
2329 + {
2330 + switch ( $atts['orderby'] )
2331 + {
2332 + case 'date':
2333 + $orderby = 'date';
2334 + break;
2335 + case 'meta_value_num':
2336 +
2337 + switch ( $atts['meta_key'] )
2338 + {
2339 + case '_price_actual':
2340 + $orderby = 'price';
2341 + break;
2342 + case '_floor_area_from_sqft':
2343 + $orderby = 'floor_area';
2344 + break;
2345 + }
2346 +
2347 + if ( $orderby != '' && !empty($atts['order']) )
2348 + {
2349 + $orderby .= '-' . $atts['order'];
2350 + }
2351 + break;
2352 + }
2353 + }
2354 +
2355 + return array( $args, $orderby );
1222 2356 }
1223 2357 }