PluginProbe
Property Hive / trunk
Property Hive vtrunk
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 +1210 -99 1.4.53trunk 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 }
@@ -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,31 +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
673 + do_action('propertyhive_shortcode_properties_before_catalog_ordering', $atts);
674 +
675 + if ( isset($atts['show_order']) && $atts['show_order'] != '' )
676 + {
677 + list( $args, $orderby ) = self::get_show_order_args( $atts, $args );
678 +
679 + propertyhive_catalog_ordering( $atts['department'], $orderby );
680 + }
681 +
682 + do_action('propertyhive_shortcode_properties_after_catalog_ordering', $atts);
683 +
402 684 $args = apply_filters( 'propertyhive_properties_query', $args, $atts );
403 685 $args = apply_filters( 'propertyhive_shortcode_properties_query', $args, $atts );
404 686
405 687 $properties = new WP_Query( $args );
406 688
407 - $propertyhive_loop['columns'] = $atts['columns'];
689 + if ( isset($atts['show_result_count']) && $atts['show_result_count'] != '' )
690 + {
691 + $total_posts = $properties->found_posts;
408 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 +
409 703 if ( $properties->have_posts() ) : ?>
410 704
411 - <?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 + ?>
412 716
413 717 <?php while ( $properties->have_posts() ) : $properties->the_post(); ?>
414 718
415 719 <?php ph_get_template_part( 'content', 'property' ); ?>
@@ -419,17 +723,22 @@
419 723 <?php propertyhive_property_loop_end(); ?>
420 724
421 725 <?php else: ?>
422 726
423 - <?php echo $atts['no_results_output']; ?>
727 + <p class="propertyhive-info no-results-message"><?php echo wp_kses_post($atts['no_results_output']); ?></p>
424 728
425 729 <?php endif;
426 730
731 + if ( isset($atts['pagination']) && $atts['pagination'] != '' )
732 + {
733 + propertyhive_pagination( $properties->max_num_pages );
734 + }
735 +
427 736 wp_reset_postdata();
428 737
429 738 $shortcode_output = ob_get_clean();
430 739
431 - 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 );
432 741 }
433 742
434 743 /**
435 744 * Recent Properties shortcode
@@ -444,27 +753,96 @@
444 753 $atts = shortcode_atts( array(
445 754 'per_page' => '12',
446 755 'columns' => '4',
447 756 'department' => '',
757 + 'minimum_price' => '',
448 758 'office_id' => '',
449 759 'negotiator_id' => '',
450 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' => '',
451 769 'orderby' => 'date',
452 770 'order' => 'desc',
453 771 'no_results_output' => '',
772 + 'pagination' => '',
773 + 'show_order' => '',
774 + 'show_result_count' => '',
775 + 'carousel' => '',
454 776 ), $atts, 'recent_properties' );
455 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 +
456 803 $meta_query = PH()->query->get_meta_query();
457 804
458 805 if ( isset($atts['department']) && $atts['department'] != '' )
459 806 {
807 + $departments = explode(",", $atts['department']);
808 + $departments = array_map('trim', $departments);
809 + $departments = array_filter($departments);
810 +
460 811 $meta_query[] = array(
461 812 'key' => '_department',
462 - 'value' => $atts['department'],
463 - 'compare' => '='
813 + 'value' => $departments,
814 + 'compare' => 'IN'
464 815 );
465 816 }
466 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 +
467 845 if ( isset($atts['office_id']) && $atts['office_id'] != '' )
468 846 {
469 847 $meta_query[] = array(
470 848 'key' => '_office_id',
@@ -481,8 +859,26 @@
481 859 'compare' => 'IN',
482 860 );
483 861 }
484 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 +
485 881 $tax_query = array();
486 882
487 883 if ( isset($atts['availability_id']) && $atts['availability_id'] != '' )
488 884 {
@@ -492,28 +888,137 @@
492 888 'compare' => 'IN',
493 889 );
494 890 }
495 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 +
496 951 $args = array(
497 952 'post_type' => 'property',
498 953 'post_status' => ( ( is_user_logged_in() && current_user_can( 'manage_propertyhive' ) ) ? array('publish', 'private') : 'publish' ),
499 954 'ignore_sticky_posts' => 1,
500 955 'posts_per_page' => $atts['per_page'],
956 + 'paged' => $paged,
501 957 'orderby' => $atts['orderby'],
502 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.
503 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.
504 962 'tax_query' => $tax_query,
963 + 'has_password' => false,
505 964 );
506 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 +
507 985 ob_start();
508 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 +
509 994 $properties = new WP_Query( apply_filters( 'propertyhive_shortcode_recent_properties_query', $args, $atts ) );
510 995
511 - $propertyhive_loop['columns'] = $atts['columns'];
996 + if ( isset($atts['show_result_count']) && $atts['show_result_count'] != '' )
997 + {
998 + $total_posts = $properties->found_posts;
512 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 +
513 1008 if ( $properties->have_posts() ) : ?>
514 1009
515 - <?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 + ?>
516 1021
517 1022 <?php while ( $properties->have_posts() ) : $properties->the_post(); ?>
518 1023
519 1024 <?php ph_get_template_part( 'content', 'property-recent' ); ?>
@@ -523,17 +1028,22 @@
523 1028 <?php propertyhive_property_loop_end(); ?>
524 1029
525 1030 <?php else: ?>
526 1031
527 - <?php echo $atts['no_results_output']; ?>
1032 + <p class="propertyhive-info no-results-message"><?php echo wp_kses_post($atts['no_results_output']); ?></p>
528 1033
529 1034 <?php endif;
530 1035
1036 + if ( isset($atts['pagination']) && $atts['pagination'] != '' )
1037 + {
1038 + propertyhive_pagination( $properties->max_num_pages );
1039 + }
1040 +
531 1041 wp_reset_postdata();
532 1042
533 1043 $shortcode_output = ob_get_clean();
534 1044
535 - 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 );
536 1046
537 1047 }
538 1048
539 1049 /**
@@ -549,24 +1059,62 @@
549 1059 $atts = shortcode_atts( array(
550 1060 'per_page' => '12',
551 1061 'columns' => '4',
552 1062 'department' => '',
1063 + 'address_keyword' => '',
553 1064 'office_id' => '',
554 1065 'negotiator_id' => '',
555 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' => '',
556 1069 'orderby' => 'rand',
557 1070 'order' => 'desc',
1071 + // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- Shortcode default declaration only; no query executes here.
558 1072 'meta_key' => '',
559 1073 'no_results_output' => '',
1074 + 'pagination' => '',
1075 + 'show_order' => '',
1076 + 'show_result_count' => '',
1077 + 'carousel' => '',
560 1078 ), $atts, 'featured_properties' );
561 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 +
562 1108 $args = array(
563 1109 'post_type' => 'property',
564 1110 'post_status' => ( ( is_user_logged_in() && current_user_can( 'manage_propertyhive' ) ) ? array('publish', 'private') : 'publish' ),
565 1111 'ignore_sticky_posts' => 1,
566 1112 'posts_per_page' => $atts['per_page'],
1113 + 'paged' => $paged,
567 1114 'orderby' => $atts['orderby'],
568 1115 'order' => $atts['order'],
1116 + 'has_password' => false,
569 1117 );
570 1118
571 1119 $meta_query = array(
572 1120 array(
@@ -580,15 +1128,98 @@
580 1128 );
581 1129
582 1130 if ( isset($atts['department']) && $atts['department'] != '' )
583 1131 {
1132 + $departments = explode(",", $atts['department']);
1133 + $departments = array_map('trim', $departments);
1134 + $departments = array_filter($departments);
1135 +
584 1136 $meta_query[] = array(
585 1137 'key' => '_department',
586 - 'value' => $atts['department'],
587 - 'compare' => '='
1138 + 'value' => $departments,
1139 + 'compare' => 'IN'
588 1140 );
589 1141 }
590 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 +
591 1222 if ( isset($atts['office_id']) && $atts['office_id'] != '' )
592 1223 {
593 1224 $meta_query[] = array(
594 1225 'key' => '_office_id',
@@ -605,11 +1236,13 @@
605 1236 'compare' => 'IN',
606 1237 );
607 1238 }
608 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.
609 1241 $args['meta_query'] = $meta_query;
610 1242
611 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.
612 1245 $args['meta_key'] = $atts['meta_key'];
613 1246 }
614 1247
615 1248 $tax_query = array();
@@ -623,20 +1256,67 @@
623 1256 );
624 1257 }
625 1258
626 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.
627 1261 $args['tax_query'] = $tax_query;
628 1262 }
629 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 +
630 1283 ob_start();
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 + }
631 1291
632 1292 $properties = new WP_Query( apply_filters( 'propertyhive_shortcode_featured_properties_query', $args, $atts ) );
633 1293
634 - $propertyhive_loop['columns'] = $atts['columns'];
1294 + if ( isset($atts['show_result_count']) && $atts['show_result_count'] != '' )
1295 + {
1296 + $total_posts = $properties->found_posts;
635 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 +
636 1306 if ( $properties->have_posts() ) : ?>
637 1307
638 - <?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 + ?>
639 1319
640 1320 <?php while ( $properties->have_posts() ) : $properties->the_post(); ?>
641 1321
642 1322 <?php ph_get_template_part( 'content', 'property-featured' ); ?>
@@ -646,17 +1326,22 @@
646 1326 <?php propertyhive_property_loop_end(); ?>
647 1327
648 1328 <?php else: ?>
649 1329
650 - <?php echo $atts['no_results_output']; ?>
1330 + <p class="propertyhive-info no-results-message"><?php echo wp_kses_post($atts['no_results_output']); ?></p>
651 1331
652 1332 <?php endif;
653 1333
1334 + if ( isset($atts['pagination']) && $atts['pagination'] != '' )
1335 + {
1336 + propertyhive_pagination( $properties->max_num_pages );
1337 + }
1338 +
654 1339 wp_reset_postdata();
655 1340
656 1341 $shortcode_output = ob_get_clean();
657 1342
658 - 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 );
659 1344 }
660 1345
661 1346 /**
662 1347 * Output similar properties
@@ -665,8 +1350,10 @@
665 1350 * @return string
666 1351 */
667 1352 public static function similar_properties( $atts ) {
668 1353
1354 + global $property, $propertyhive_loop;
1355 +
669 1356 $atts = shortcode_atts( array(
670 1357 'per_page' => '2',
671 1358 'columns' => '2',
672 1359 'orderby' => 'rand',
@@ -672,38 +1359,56 @@
672 1359 'orderby' => 'rand',
673 1360 'order' => 'asc',
674 1361 'price_percentage_bounds' => 10,
675 1362 'bedroom_bounds' => 0,
1363 + 'matching_address_field' => '', // only return fields with matching address field. Options: address_two, address_three, address_four, location
676 1364 'property_id' => '',
677 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' => '',
678 1370 'no_results_output' => '',
1371 + 'carousel' => '',
679 1372 ), $atts, 'similar_properties' );
680 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 +
681 1404 if ($atts['property_id'] != '')
682 1405 {
683 1406 $department = get_post_meta( $atts['property_id'], '_department', true );
684 1407
685 - $price = get_post_meta( $atts['property_id'], '_price_actual', true );
686 - $lower_price = $price;
687 - $higher_price = $price;
688 - $atts['price_percentage_bounds'] = str_replace("%", "", $atts['price_percentage_bounds']);
689 - if ( isset($atts['price_percentage_bounds']) && $atts['price_percentage_bounds'] != '' && is_numeric($atts['price_percentage_bounds']) && $atts['price_percentage_bounds'] > 0 )
690 - {
691 - $lower_price = $price - ($price * $atts['price_percentage_bounds'] / 100);
692 - $higher_price = $price + ($price * $atts['price_percentage_bounds'] / 100);
693 - }
694 -
695 - $bedrooms = get_post_meta( $atts['property_id'], '_bedrooms', true );
696 - $lower_bedrooms = $bedrooms;
697 - $higher_bedrooms = $bedrooms;
698 - if ( isset($atts['bedroom_bounds']) && $atts['bedroom_bounds'] != '' && is_numeric($atts['bedroom_bounds']) && $atts['bedroom_bounds'] > 0 )
699 - {
700 - $lower_bedrooms = $bedrooms - $atts['bedroom_bounds'];
701 - $higher_bedrooms = $bedrooms + $atts['bedroom_bounds'];
702 - }
703 -
704 1408 $args = array(
705 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.
706 1411 'post__not_in' => array($atts['property_id']),
707 1412 'post_status' => ( ( is_user_logged_in() && current_user_can( 'manage_propertyhive' ) ) ? array('publish', 'private') : 'publish' ),
708 1413 'ignore_sticky_posts' => 1,
709 1414 'posts_per_page' => $atts['per_page'],
@@ -708,8 +1413,9 @@
708 1413 'ignore_sticky_posts' => 1,
709 1414 'posts_per_page' => $atts['per_page'],
710 1415 'orderby' => $atts['orderby'],
711 1416 'order' => $atts['order'],
1417 + 'has_password' => false,
712 1418 );
713 1419
714 1420 $meta_query = array();
715 1421
@@ -722,42 +1428,218 @@
722 1428 'key' => '_on_market',
723 1429 'value' => 'yes',
724 1430 );
725 1431
726 - if ( isset($atts['bedroom_bounds']) && is_numeric($atts['bedroom_bounds']) )
1432 + if ( $department != 'commercial' && ph_get_custom_department_based_on( $department ) != 'commercial' )
727 1433 {
728 - $meta_query[] = array(
729 - 'key' => '_bedrooms',
730 - 'value' => $lower_bedrooms,
731 - 'compare' => '>=',
732 - 'type' => 'NUMERIC'
733 - );
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 + }
734 1443
735 - $meta_query[] = array(
736 - 'key' => '_bedrooms',
737 - 'value' => $higher_bedrooms,
738 - 'compare' => '<=',
739 - 'type' => 'NUMERIC'
740 - );
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 + }
741 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 );
742 1479
743 - 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' )) )
744 1632 {
745 - $meta_query[] = array(
746 - 'key' => '_price_actual',
747 - 'value' => $lower_price,
748 - 'compare' => '>=',
749 - 'type' => 'NUMERIC'
750 - );
1633 + $address_field = get_post_meta( $atts['property_id'], '_' . $atts['matching_address_field'], true );
751 1634
752 1635 $meta_query[] = array(
753 - 'key' => '_price_actual',
754 - 'value' => $higher_price,
755 - 'compare' => '<=',
756 - 'type' => 'NUMERIC'
1636 + 'key' => '_' . $atts['matching_address_field'],
1637 + 'value' => $address_field,
757 1638 );
758 1639 }
759 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.
760 1642 $args['meta_query'] = $meta_query;
761 1643
762 1644 $tax_query = array();
763 1645
@@ -769,21 +1651,104 @@
769 1651 'compare' => 'IN',
770 1652 );
771 1653 }
772 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 +
773 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.
774 1710 $args['tax_query'] = $tax_query;
775 1711 }
776 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 +
777 1732 ob_start();
778 1733
779 1734 $properties = new WP_Query( apply_filters( 'propertyhive_shortcode_similar_properties_query', $args, $atts ) );
780 1735
781 - $propertyhive_loop['columns'] = $atts['columns'];
1736 + $propertyhive_loop['columns'] = (int)$atts['columns'];
782 1737
783 1738 if ( $properties->have_posts() ) : ?>
784 1739
785 - <?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 + ?>
786 1751
787 1752 <?php while ( $properties->have_posts() ) : $properties->the_post(); ?>
788 1753
789 1754 <?php ph_get_template_part( 'content', 'property-featured' ); ?>
@@ -793,9 +1758,9 @@
793 1758 <?php propertyhive_property_loop_end(); ?>
794 1759
795 1760 <?php else: ?>
796 1761
797 - <?php echo $atts['no_results_output']; ?>
1762 + <p class="propertyhive-info no-results-message"><?php echo wp_kses_post($atts['no_results_output']); ?></p>
798 1763
799 1764 <?php endif;
800 1765
801 1766 wp_reset_postdata();
@@ -806,9 +1771,9 @@
806 1771 }
807 1772
808 1773 $shortcode_output = ob_get_clean();
809 1774
810 - 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 );
811 1776 }
812 1777
813 1778 /**
814 1779 * Output property map
@@ -824,19 +1789,46 @@
824 1789 $atts = shortcode_atts( array(
825 1790 'id' => '',
826 1791 'height' => '400',
827 1792 'zoom' => '14',
828 - 'scrollwheel' => 'true'
1793 + 'scrollwheel' => 'true',
1794 + 'init_on_load' => 'true',
1795 + 'embed' => 'false'
829 1796 ), $atts, 'property_map' );
830 1797
831 1798 ob_start();
832 1799
833 - echo get_property_map( $atts );
1800 + get_property_map( $atts );
834 1801
835 1802 return ob_get_clean();
836 1803 }
837 1804
838 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 + /**
839 1831 * Output property street view
840 1832 * Should only be used on a property page or where the $property var is set
841 1833 *
842 1834 * @param array $atts
@@ -847,13 +1839,15 @@
847 1839 global $property;
848 1840
849 1841 $atts = shortcode_atts( array(
850 1842 'height' => '400',
1843 + 'init_on_load' => 'true',
1844 + 'embed' => 'false'
851 1845 ), $atts, 'property_street_view' );
852 1846
853 1847 ob_start();
854 1848
855 - echo get_property_street_view( $atts );
1849 + get_property_street_view( $atts );
856 1850
857 1851 return ob_get_clean();
858 1852 }
859 1853
@@ -884,24 +1878,24 @@
884 1878 echo '<div class="property-office-details">';
885 1879
886 1880 if ( $property->office_name != '' )
887 1881 {
888 - echo '<div class="office-name">' . $property->office_name . '</div>';
1882 + echo '<div class="office-name">' . esc_html($property->office_name) . '</div>';
889 1883 }
890 1884
891 1885 if ( $property->get_office_address( $atts['address_separator'] ) != '' )
892 1886 {
893 - 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>';
894 1888 }
895 1889
896 1890 if ( $property->office_telephone_number != '' )
897 1891 {
898 - 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>';
899 1893 }
900 1894
901 1895 if ( $property->office_email_address != '' )
902 1896 {
903 - 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>';
904 1898 }
905 1899
906 1900 echo '</div>';
907 1901 }
@@ -954,12 +1948,12 @@
954 1948
955 1949 ob_start();
956 1950
957 1951 $api_key = get_option('propertyhive_google_maps_api_key', '');
958 - 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 );
959 1953 wp_enqueue_script('googlemaps');
960 1954
961 - 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>';
962 1956 ?>
963 1957 <script>
964 1958
965 1959 // We declare vars globally so developers can access them
@@ -980,8 +1974,9 @@
980 1974 $args['p'] = (int)$atts['office_id'];
981 1975 }
982 1976 else
983 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.
984 1979 $args['meta_query'] = array(
985 1980 array(
986 1981 'key' => 'primary',
987 1982 'value' => '1'
@@ -1009,11 +2004,11 @@
1009 2004 $lat = '51.509865';
1010 2005 $lng = '-0.118092';
1011 2006 }
1012 2007 ?>
1013 - 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; ?>);
1014 2009 var map_options = {
1015 - 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' ); ?>,
1016 2011 center: myLatlng,
1017 2012 mapTypeId: google.maps.MapTypeId.ROADMAP,
1018 2013 scrollwheel: <?php echo ( ( isset($atts['scrollwheel']) && ($atts['scrollwheel'] === 'false' || $atts['scrollwheel'] === FALSE) ) ? 'false' : 'true' ); ?>
1019 2014 }
@@ -1021,11 +2016,15 @@
1021 2016 if ( class_exists( 'PH_Map_Search' ) )
1022 2017 {
1023 2018 $map_add_on_settings = get_option( 'propertyhive_map_search', array() );
1024 2019
1025 - 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']) != '' )
1026 2021 {
1027 - 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 + }
1028 2027 }
1029 2028 }
1030 2029
1031 2030 do_action( 'propertyhive_office_map_options' );
@@ -1056,9 +2055,9 @@
1056 2055
1057 2056 if ( $lat != '' && $lng != '' )
1058 2057 {
1059 2058 ?>
1060 - 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; ?>);
1061 2060
1062 2061 var marker_options = {
1063 2062 map: office_map,
1064 2063 position: myLatlng,
@@ -1074,9 +2073,9 @@
1074 2073 {
1075 2074 $marker_icon_url = wp_get_attachment_url( $map_add_on_settings['custom_icon_attachment_id'] );
1076 2075 if ( $marker_icon_url !== FALSE )
1077 2076 {
1078 - echo 'marker_options.icon = \'' . $marker_icon_url . '\';';
2077 + echo 'marker_options.icon = \'' . esc_url($marker_icon_url) . '\';';
1079 2078 }
1080 2079 }
1081 2080 }
1082 2081 ?>
@@ -1147,9 +2146,9 @@
1147 2146 $form_controls = apply_filters( 'propertyhive_user_details_form_fields', $form_controls );
1148 2147
1149 2148 $form_controls_2 = ph_get_applicant_requirements_form_fields();
1150 2149
1151 - $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 );
1152 2151
1153 2152 $form_controls = array_merge( $form_controls, $form_controls_2 );
1154 2153
1155 2154 if ( get_option( 'propertyhive_applicant_registration_form_disclaimer', '' ) != '' )
@@ -1207,8 +2206,67 @@
1207 2206
1208 2207 }
1209 2208
1210 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 + /**
1211 2269 * Output 'My Account' page
1212 2270 *
1213 2271 * @param array $atts
1214 2272 * @return string
@@ -1241,6 +2299,59 @@
1241 2299 ph_get_template( 'account/my-account.php' );
1242 2300
1243 2301 return ob_get_clean();
1244 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 );
1245 2356 }
1246 2357 }