PluginProbe
Property Hive / 2.3.0
Property Hive v2.3.0
2.3.1 2.3.0 2.2.6 2.2.5 2.2.4 2.2.3 2.2.2 1.4.46 1.4.47 1.4.48 1.4.49 1.4.5 1.4.50 1.4.51 1.4.52 1.4.53 1.4.54 1.4.55 1.4.56 1.4.57 1.4.58 1.4.59 1.4.6 1.4.60 1.4.61 All 261 releases
← All changes | includes/class-ph-shortcodes.php +1207 -99 1.4.622.3.0 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 }
@@ -96,9 +104,10 @@
96 104 $form_controls = apply_filters( 'propertyhive_search_form_fields_after_' . $atts['id'], $form_controls, $atts );
97 105 $form_controls = apply_filters( 'propertyhive_search_form_fields_after', $form_controls, $atts );
98 106
99 107 if (
100 - 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.
101 110 ( !isset($_REQUEST['department']) )
102 111 )
103 112 {
104 113 $form_controls['department']['value'] = $atts['default_department'];
@@ -117,23 +126,34 @@
117 126 * @param array $atts
118 127 * @return string
119 128 */
120 129 public static function properties( $atts ) {
130 +
131 + global $propertyhive_loop;
132 +
121 133 $atts = shortcode_atts( array(
122 134 'columns' => '2',
123 135 'orderby' => 'meta_value_num',
124 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.
125 138 'meta_key' => '_price_actual',
126 139 'ids' => '',
127 - '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
128 143 'minimum_price' => '',
129 144 'maximum_price' => '',
130 145 'bedrooms' => '',
146 + 'minimum_bedrooms' => '',
147 + 'keyword' => '',
131 148 'address_keyword' => '',
149 + 'country' => '',
150 + 'country_not' => '',
132 151 'availability_id' => '',
133 152 'marketing_flag' => '', // Deprecated. Use marketing_flag_id instead
134 153 'marketing_flag_id' => '', // Should be marketing_flag_id. Might deprecate this in the future
135 154 'property_type_id' => '',
155 + 'sale_by_id' => '',
136 156 'location_id' => '',
137 157 'office_id' => '',
138 158 'negotiator_id' => '',
139 159 'commercial_for_sale' => '',
@@ -139,10 +159,39 @@
139 159 'commercial_for_sale' => '',
140 160 'commercial_to_rent' => '',
141 161 'posts_per_page' => 10,
142 162 'no_results_output' => '',
163 + 'pagination' => '',
164 + 'show_order' => '',
165 + 'show_result_count' => '',
166 + 'carousel' => '',
143 167 ), $atts, 'properties' );
144 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 +
145 194 $meta_query = array(
146 195 array(
147 196 'key' => '_on_market',
148 197 'value' => 'yes',
@@ -148,14 +197,18 @@
148 197 'value' => 'yes',
149 198 )
150 199 );
151 200
152 - 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() )) )
153 202 {
203 + $departments = explode(",", $atts['department']);
204 + $departments = array_map('trim', $departments);
205 + $departments = array_filter($departments);
206 +
154 207 $meta_query[] = array(
155 208 'key' => '_department',
156 - 'value' => $atts['department'],
157 - 'compare' => '='
209 + 'value' => $departments,
210 + 'compare' => 'IN'
158 211 );
159 212 }
160 213
161 214 if ( isset($atts['bedrooms']) && $atts['bedrooms'] != '' && is_numeric($atts['bedrooms']) )
@@ -162,13 +215,30 @@
162 215 {
163 216 $meta_query[] = array(
164 217 'key' => '_bedrooms',
165 218 'value' => sanitize_text_field( $atts['bedrooms'] ),
166 - 'compare' => '='
219 + 'compare' => '=',
220 + 'type' => 'NUMERIC'
167 221 );
168 222 }
169 223
170 - 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'] != '' )
171 241 {
172 242 $search_form_currency = get_option( 'propertyhive_search_form_currency', 'GBP' );
173 243
174 244 $minimum_price = $atts['minimum_price'];
@@ -187,9 +257,9 @@
187 257 'type' => 'NUMERIC'
188 258 );
189 259 }
190 260
191 - 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'] != '' )
192 262 {
193 263 $search_form_currency = get_option( 'propertyhive_search_form_currency', 'GBP' );
194 264
195 265 $maximum_price = $atts['maximum_price'];
@@ -210,9 +280,9 @@
210 280 }
211 281
212 282 if ( isset($atts['address_keyword']) && $atts['address_keyword'] != '' )
213 283 {
214 - $atts['address_keyword'] = sanitize_text_field( trim( $atts['address_keyword'] ) );
284 + $atts['address_keyword'] = ph_clean( trim( $atts['address_keyword'] ) );
215 285
216 286 $address_keywords = array( $atts['address_keyword'] );
217 287
218 288 if ( strpos( $atts['address_keyword'], ' ' ) !== FALSE )
@@ -222,37 +292,55 @@
222 292 if ( strpos( $atts['address_keyword'], '-' ) !== FALSE )
223 293 {
224 294 $address_keywords[] = str_replace("-", " ", $atts['address_keyword']);
225 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 + }
226 308
227 309 $sub_meta_query = array('relation' => 'OR');
228 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 +
229 317 foreach ( $address_keywords as $address_keyword )
230 318 {
231 319 $sub_meta_query[] = array(
232 320 'key' => '_reference_number',
233 321 'value' => $address_keyword,
234 - 'compare' => get_option( 'propertyhive_address_keyword_compare', '=' )
322 + 'compare' => $address_keyword_compare
235 323 );
236 324 $sub_meta_query[] = array(
237 325 'key' => '_address_street',
238 326 'value' => $address_keyword,
239 - 'compare' => get_option( 'propertyhive_address_keyword_compare', '=' )
327 + 'compare' => $address_keyword_compare
240 328 );
241 329 $sub_meta_query[] = array(
242 330 'key' => '_address_two',
243 331 'value' => $address_keyword,
244 - 'compare' => get_option( 'propertyhive_address_keyword_compare', '=' )
332 + 'compare' => $address_keyword_compare
245 333 );
246 334 $sub_meta_query[] = array(
247 335 'key' => '_address_three',
248 336 'value' => $address_keyword,
249 - 'compare' => get_option( 'propertyhive_address_keyword_compare', '=' )
337 + 'compare' => $address_keyword_compare
250 338 );
251 339 $sub_meta_query[] = array(
252 340 'key' => '_address_four',
253 341 'value' => $address_keyword,
254 - 'compare' => get_option( 'propertyhive_address_keyword_compare', '=' )
342 + 'compare' => $address_keyword_compare
255 343 );
256 344 }
257 345 if ( strlen($atts['address_keyword']) <= 4 )
258 346 {
@@ -260,11 +348,14 @@
260 348 'key' => '_address_postcode',
261 349 'value' => sanitize_text_field( $atts['address_keyword'] ),
262 350 'compare' => '='
263 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 [ ]
264 355 $sub_meta_query[] = array(
265 356 'key' => '_address_postcode',
266 - 'value' => sanitize_text_field( $atts['address_keyword'] ) . '[ ]',
357 + 'value' => sanitize_text_field( $atts['address_keyword'] ) . '[a-zA-Z]?[ ]',
267 358 'compare' => 'RLIKE'
268 359 );
269 360 }
270 361 else
@@ -278,8 +369,135 @@
278 369
279 370 $meta_query[] = $sub_meta_query;
280 371 }
281 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 +
282 500 if ( isset($atts['office_id']) && $atts['office_id'] != '' )
283 501 {
284 502 $meta_query[] = array(
285 503 'key' => '_office_id',
@@ -341,9 +559,18 @@
341 559 }
342 560
343 561 if ( isset($atts['property_type_id']) && $atts['property_type_id'] != '' )
344 562 {
345 - 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 + )
346 573 {
347 574 $tax_query[] = array(
348 575 'taxonomy' => 'commercial_property_type',
349 576 'terms' => explode(",", $atts['property_type_id']),
@@ -368,11 +595,20 @@
368 595 'compare' => 'IN',
369 596 );
370 597 }
371 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 +
372 608 // Change default meta key when department is specified as commercial, or if commercial is the only active department
373 609 if (
374 - ( isset($atts['department']) && $atts['department'] == 'commercial' ) ||
610 + ( isset($atts['department']) && $base_department == 'commercial' ) ||
375 611 (
376 612 get_option( 'propertyhive_active_departments_sales' ) != 'yes' &&
377 613 get_option( 'propertyhive_active_departments_lettings' ) != 'yes' &&
378 614 get_option( 'propertyhive_active_departments_commercial' ) == 'yes'
@@ -378,11 +614,15 @@
378 614 get_option( 'propertyhive_active_departments_commercial' ) == 'yes'
379 615 )
380 616 )
381 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.
382 619 $atts['meta_key'] = '_floor_area_from_sqft';
383 620 }
384 621
622 + // Get which page we're currently viewing from the URL
623 + $paged = max( 1, get_query_var( 'paged' ) );
624 +
385 625 $args = array(
386 626 'post_type' => 'property',
387 627 'post_status' => ( ( is_user_logged_in() && current_user_can( 'manage_propertyhive' ) ) ? array('publish', 'private') : 'publish' ),
388 628 'ignore_sticky_posts' => 1,
@@ -388,31 +628,92 @@
388 628 'ignore_sticky_posts' => 1,
389 629 'orderby' => $atts['orderby'],
390 630 'order' => $atts['order'],
391 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.
392 634 'meta_query' => $meta_query,
393 - '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,
394 638 );
395 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.
396 641 $args['meta_key'] = $atts['meta_key'];
397 642 }
398 643
399 - if ( ! empty( $atts['ids'] ) ) {
400 - $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 + }
401 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 + }
402 668
669 + $args['orderby'] .= ' post_title';
670 +
403 671 ob_start();
404 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 +
405 684 $args = apply_filters( 'propertyhive_properties_query', $args, $atts );
406 685 $args = apply_filters( 'propertyhive_shortcode_properties_query', $args, $atts );
407 686
408 687 $properties = new WP_Query( $args );
409 688
410 - $propertyhive_loop['columns'] = $atts['columns'];
689 + if ( isset($atts['show_result_count']) && $atts['show_result_count'] != '' )
690 + {
691 + $total_posts = $properties->found_posts;
411 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 +
412 703 if ( $properties->have_posts() ) : ?>
413 704
414 - <?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 + ?>
415 716
416 717 <?php while ( $properties->have_posts() ) : $properties->the_post(); ?>
417 718
418 719 <?php ph_get_template_part( 'content', 'property' ); ?>
@@ -422,17 +723,22 @@
422 723 <?php propertyhive_property_loop_end(); ?>
423 724
424 725 <?php else: ?>
425 726
426 - <?php echo $atts['no_results_output']; ?>
727 + <p class="propertyhive-info no-results-message"><?php echo wp_kses_post($atts['no_results_output']); ?></p>
427 728
428 729 <?php endif;
429 730
731 + if ( isset($atts['pagination']) && $atts['pagination'] != '' )
732 + {
733 + propertyhive_pagination( $properties->max_num_pages );
734 + }
735 +
430 736 wp_reset_postdata();
431 737
432 738 $shortcode_output = ob_get_clean();
433 739
434 - 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 );
435 741 }
436 742
437 743 /**
438 744 * Recent Properties shortcode
@@ -447,27 +753,96 @@
447 753 $atts = shortcode_atts( array(
448 754 'per_page' => '12',
449 755 'columns' => '4',
450 756 'department' => '',
757 + 'minimum_price' => '',
451 758 'office_id' => '',
452 759 'negotiator_id' => '',
453 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' => '',
454 769 'orderby' => 'date',
455 770 'order' => 'desc',
456 771 'no_results_output' => '',
772 + 'pagination' => '',
773 + 'show_order' => '',
774 + 'show_result_count' => '',
775 + 'carousel' => '',
457 776 ), $atts, 'recent_properties' );
458 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 +
459 803 $meta_query = PH()->query->get_meta_query();
460 804
461 805 if ( isset($atts['department']) && $atts['department'] != '' )
462 806 {
807 + $departments = explode(",", $atts['department']);
808 + $departments = array_map('trim', $departments);
809 + $departments = array_filter($departments);
810 +
463 811 $meta_query[] = array(
464 812 'key' => '_department',
465 - 'value' => $atts['department'],
466 - 'compare' => '='
813 + 'value' => $departments,
814 + 'compare' => 'IN'
467 815 );
468 816 }
469 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 +
470 845 if ( isset($atts['office_id']) && $atts['office_id'] != '' )
471 846 {
472 847 $meta_query[] = array(
473 848 'key' => '_office_id',
@@ -484,8 +859,26 @@
484 859 'compare' => 'IN',
485 860 );
486 861 }
487 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 +
488 881 $tax_query = array();
489 882
490 883 if ( isset($atts['availability_id']) && $atts['availability_id'] != '' )
491 884 {
@@ -495,28 +888,137 @@
495 888 'compare' => 'IN',
496 889 );
497 890 }
498 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 +
499 951 $args = array(
500 952 'post_type' => 'property',
501 953 'post_status' => ( ( is_user_logged_in() && current_user_can( 'manage_propertyhive' ) ) ? array('publish', 'private') : 'publish' ),
502 954 'ignore_sticky_posts' => 1,
503 955 'posts_per_page' => $atts['per_page'],
956 + 'paged' => $paged,
504 957 'orderby' => $atts['orderby'],
505 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.
506 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.
507 962 'tax_query' => $tax_query,
963 + 'has_password' => false,
508 964 );
509 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 +
510 985 ob_start();
511 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 +
512 994 $properties = new WP_Query( apply_filters( 'propertyhive_shortcode_recent_properties_query', $args, $atts ) );
513 995
514 - $propertyhive_loop['columns'] = $atts['columns'];
996 + if ( isset($atts['show_result_count']) && $atts['show_result_count'] != '' )
997 + {
998 + $total_posts = $properties->found_posts;
515 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 +
516 1008 if ( $properties->have_posts() ) : ?>
517 1009
518 - <?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 + ?>
519 1021
520 1022 <?php while ( $properties->have_posts() ) : $properties->the_post(); ?>
521 1023
522 1024 <?php ph_get_template_part( 'content', 'property-recent' ); ?>
@@ -526,17 +1028,22 @@
526 1028 <?php propertyhive_property_loop_end(); ?>
527 1029
528 1030 <?php else: ?>
529 1031
530 - <?php echo $atts['no_results_output']; ?>
1032 + <p class="propertyhive-info no-results-message"><?php echo wp_kses_post($atts['no_results_output']); ?></p>
531 1033
532 1034 <?php endif;
533 1035
1036 + if ( isset($atts['pagination']) && $atts['pagination'] != '' )
1037 + {
1038 + propertyhive_pagination( $properties->max_num_pages );
1039 + }
1040 +
534 1041 wp_reset_postdata();
535 1042
536 1043 $shortcode_output = ob_get_clean();
537 1044
538 - 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 );
539 1046
540 1047 }
541 1048
542 1049 /**
@@ -552,24 +1059,62 @@
552 1059 $atts = shortcode_atts( array(
553 1060 'per_page' => '12',
554 1061 'columns' => '4',
555 1062 'department' => '',
1063 + 'address_keyword' => '',
556 1064 'office_id' => '',
557 1065 'negotiator_id' => '',
558 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' => '',
559 1069 'orderby' => 'rand',
560 1070 'order' => 'desc',
1071 + // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- Shortcode default declaration only; no query executes here.
561 1072 'meta_key' => '',
562 1073 'no_results_output' => '',
1074 + 'pagination' => '',
1075 + 'show_order' => '',
1076 + 'show_result_count' => '',
1077 + 'carousel' => '',
563 1078 ), $atts, 'featured_properties' );
564 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 +
565 1108 $args = array(
566 1109 'post_type' => 'property',
567 1110 'post_status' => ( ( is_user_logged_in() && current_user_can( 'manage_propertyhive' ) ) ? array('publish', 'private') : 'publish' ),
568 1111 'ignore_sticky_posts' => 1,
569 1112 'posts_per_page' => $atts['per_page'],
1113 + 'paged' => $paged,
570 1114 'orderby' => $atts['orderby'],
571 1115 'order' => $atts['order'],
1116 + 'has_password' => false,
572 1117 );
573 1118
574 1119 $meta_query = array(
575 1120 array(
@@ -583,15 +1128,98 @@
583 1128 );
584 1129
585 1130 if ( isset($atts['department']) && $atts['department'] != '' )
586 1131 {
1132 + $departments = explode(",", $atts['department']);
1133 + $departments = array_map('trim', $departments);
1134 + $departments = array_filter($departments);
1135 +
587 1136 $meta_query[] = array(
588 1137 'key' => '_department',
589 - 'value' => $atts['department'],
590 - 'compare' => '='
1138 + 'value' => $departments,
1139 + 'compare' => 'IN'
591 1140 );
592 1141 }
593 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 +
594 1222 if ( isset($atts['office_id']) && $atts['office_id'] != '' )
595 1223 {
596 1224 $meta_query[] = array(
597 1225 'key' => '_office_id',
@@ -608,11 +1236,13 @@
608 1236 'compare' => 'IN',
609 1237 );
610 1238 }
611 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.
612 1241 $args['meta_query'] = $meta_query;
613 1242
614 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.
615 1245 $args['meta_key'] = $atts['meta_key'];
616 1246 }
617 1247
618 1248 $tax_query = array();
@@ -626,20 +1256,67 @@
626 1256 );
627 1257 }
628 1258
629 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.
630 1261 $args['tax_query'] = $tax_query;
631 1262 }
632 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 +
633 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 + }
634 1291
635 1292 $properties = new WP_Query( apply_filters( 'propertyhive_shortcode_featured_properties_query', $args, $atts ) );
636 1293
637 - $propertyhive_loop['columns'] = $atts['columns'];
1294 + if ( isset($atts['show_result_count']) && $atts['show_result_count'] != '' )
1295 + {
1296 + $total_posts = $properties->found_posts;
638 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 +
639 1306 if ( $properties->have_posts() ) : ?>
640 1307
641 - <?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 + ?>
642 1319
643 1320 <?php while ( $properties->have_posts() ) : $properties->the_post(); ?>
644 1321
645 1322 <?php ph_get_template_part( 'content', 'property-featured' ); ?>
@@ -649,17 +1326,22 @@
649 1326 <?php propertyhive_property_loop_end(); ?>
650 1327
651 1328 <?php else: ?>
652 1329
653 - <?php echo $atts['no_results_output']; ?>
1330 + <p class="propertyhive-info no-results-message"><?php echo wp_kses_post($atts['no_results_output']); ?></p>
654 1331
655 1332 <?php endif;
656 1333
1334 + if ( isset($atts['pagination']) && $atts['pagination'] != '' )
1335 + {
1336 + propertyhive_pagination( $properties->max_num_pages );
1337 + }
1338 +
657 1339 wp_reset_postdata();
658 1340
659 1341 $shortcode_output = ob_get_clean();
660 1342
661 - 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 );
662 1344 }
663 1345
664 1346 /**
665 1347 * Output similar properties
@@ -668,8 +1350,10 @@
668 1350 * @return string
669 1351 */
670 1352 public static function similar_properties( $atts ) {
671 1353
1354 + global $property, $propertyhive_loop;
1355 +
672 1356 $atts = shortcode_atts( array(
673 1357 'per_page' => '2',
674 1358 'columns' => '2',
675 1359 'orderby' => 'rand',
@@ -675,38 +1359,56 @@
675 1359 'orderby' => 'rand',
676 1360 'order' => 'asc',
677 1361 'price_percentage_bounds' => 10,
678 1362 'bedroom_bounds' => 0,
1363 + 'matching_address_field' => '', // only return fields with matching address field. Options: address_two, address_three, address_four, location
679 1364 'property_id' => '',
680 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' => '',
681 1370 'no_results_output' => '',
1371 + 'carousel' => '',
682 1372 ), $atts, 'similar_properties' );
683 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 +
684 1404 if ($atts['property_id'] != '')
685 1405 {
686 1406 $department = get_post_meta( $atts['property_id'], '_department', true );
687 1407
688 - $price = get_post_meta( $atts['property_id'], '_price_actual', true );
689 - $lower_price = $price;
690 - $higher_price = $price;
691 - $atts['price_percentage_bounds'] = str_replace("%", "", $atts['price_percentage_bounds']);
692 - if ( isset($atts['price_percentage_bounds']) && $atts['price_percentage_bounds'] != '' && is_numeric($atts['price_percentage_bounds']) && $atts['price_percentage_bounds'] > 0 )
693 - {
694 - $lower_price = $price - ($price * $atts['price_percentage_bounds'] / 100);
695 - $higher_price = $price + ($price * $atts['price_percentage_bounds'] / 100);
696 - }
697 -
698 - $bedrooms = get_post_meta( $atts['property_id'], '_bedrooms', true );
699 - $lower_bedrooms = $bedrooms;
700 - $higher_bedrooms = $bedrooms;
701 - if ( isset($atts['bedroom_bounds']) && $atts['bedroom_bounds'] != '' && is_numeric($atts['bedroom_bounds']) && $atts['bedroom_bounds'] > 0 )
702 - {
703 - $lower_bedrooms = $bedrooms - $atts['bedroom_bounds'];
704 - $higher_bedrooms = $bedrooms + $atts['bedroom_bounds'];
705 - }
706 -
707 1408 $args = array(
708 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.
709 1411 'post__not_in' => array($atts['property_id']),
710 1412 'post_status' => ( ( is_user_logged_in() && current_user_can( 'manage_propertyhive' ) ) ? array('publish', 'private') : 'publish' ),
711 1413 'ignore_sticky_posts' => 1,
712 1414 'posts_per_page' => $atts['per_page'],
@@ -711,8 +1413,9 @@
711 1413 'ignore_sticky_posts' => 1,
712 1414 'posts_per_page' => $atts['per_page'],
713 1415 'orderby' => $atts['orderby'],
714 1416 'order' => $atts['order'],
1417 + 'has_password' => false,
715 1418 );
716 1419
717 1420 $meta_query = array();
718 1421
@@ -725,42 +1428,218 @@
725 1428 'key' => '_on_market',
726 1429 'value' => 'yes',
727 1430 );
728 1431
729 - if ( isset($atts['bedroom_bounds']) && is_numeric($atts['bedroom_bounds']) )
1432 + if ( $department != 'commercial' && ph_get_custom_department_based_on( $department ) != 'commercial' )
730 1433 {
731 - $meta_query[] = array(
732 - 'key' => '_bedrooms',
733 - 'value' => $lower_bedrooms,
734 - 'compare' => '>=',
735 - 'type' => 'NUMERIC'
736 - );
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 + }
737 1443
738 - $meta_query[] = array(
739 - 'key' => '_bedrooms',
740 - 'value' => $higher_bedrooms,
741 - 'compare' => '<=',
742 - 'type' => 'NUMERIC'
743 - );
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 + }
744 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 );
745 1479
746 - 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' )) )
747 1632 {
748 - $meta_query[] = array(
749 - 'key' => '_price_actual',
750 - 'value' => $lower_price,
751 - 'compare' => '>=',
752 - 'type' => 'NUMERIC'
753 - );
1633 + $address_field = get_post_meta( $atts['property_id'], '_' . $atts['matching_address_field'], true );
754 1634
755 1635 $meta_query[] = array(
756 - 'key' => '_price_actual',
757 - 'value' => $higher_price,
758 - 'compare' => '<=',
759 - 'type' => 'NUMERIC'
1636 + 'key' => '_' . $atts['matching_address_field'],
1637 + 'value' => $address_field,
760 1638 );
761 1639 }
762 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.
763 1642 $args['meta_query'] = $meta_query;
764 1643
765 1644 $tax_query = array();
766 1645
@@ -772,21 +1651,104 @@
772 1651 'compare' => 'IN',
773 1652 );
774 1653 }
775 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 +
776 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.
777 1710 $args['tax_query'] = $tax_query;
778 1711 }
779 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 +
780 1732 ob_start();
781 1733
782 1734 $properties = new WP_Query( apply_filters( 'propertyhive_shortcode_similar_properties_query', $args, $atts ) );
783 1735
784 - $propertyhive_loop['columns'] = $atts['columns'];
1736 + $propertyhive_loop['columns'] = (int)$atts['columns'];
785 1737
786 1738 if ( $properties->have_posts() ) : ?>
787 1739
788 - <?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 + ?>
789 1751
790 1752 <?php while ( $properties->have_posts() ) : $properties->the_post(); ?>
791 1753
792 1754 <?php ph_get_template_part( 'content', 'property-featured' ); ?>
@@ -796,9 +1758,9 @@
796 1758 <?php propertyhive_property_loop_end(); ?>
797 1759
798 1760 <?php else: ?>
799 1761
800 - <?php echo $atts['no_results_output']; ?>
1762 + <p class="propertyhive-info no-results-message"><?php echo wp_kses_post($atts['no_results_output']); ?></p>
801 1763
802 1764 <?php endif;
803 1765
804 1766 wp_reset_postdata();
@@ -809,9 +1771,9 @@
809 1771 }
810 1772
811 1773 $shortcode_output = ob_get_clean();
812 1774
813 - 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 );
814 1776 }
815 1777
816 1778 /**
817 1779 * Output property map
@@ -827,19 +1789,46 @@
827 1789 $atts = shortcode_atts( array(
828 1790 'id' => '',
829 1791 'height' => '400',
830 1792 'zoom' => '14',
831 - 'scrollwheel' => 'true'
1793 + 'scrollwheel' => 'true',
1794 + 'init_on_load' => 'true',
1795 + 'embed' => 'false'
832 1796 ), $atts, 'property_map' );
833 1797
834 1798 ob_start();
835 1799
836 - echo get_property_map( $atts );
1800 + get_property_map( $atts );
837 1801
838 1802 return ob_get_clean();
839 1803 }
840 1804
841 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 + /**
842 1831 * Output property street view
843 1832 * Should only be used on a property page or where the $property var is set
844 1833 *
845 1834 * @param array $atts
@@ -850,13 +1839,15 @@
850 1839 global $property;
851 1840
852 1841 $atts = shortcode_atts( array(
853 1842 'height' => '400',
1843 + 'init_on_load' => 'true',
1844 + 'embed' => 'false'
854 1845 ), $atts, 'property_street_view' );
855 1846
856 1847 ob_start();
857 1848
858 - echo get_property_street_view( $atts );
1849 + get_property_street_view( $atts );
859 1850
860 1851 return ob_get_clean();
861 1852 }
862 1853
@@ -887,24 +1878,24 @@
887 1878 echo '<div class="property-office-details">';
888 1879
889 1880 if ( $property->office_name != '' )
890 1881 {
891 - echo '<div class="office-name">' . $property->office_name . '</div>';
1882 + echo '<div class="office-name">' . esc_html($property->office_name) . '</div>';
892 1883 }
893 1884
894 1885 if ( $property->get_office_address( $atts['address_separator'] ) != '' )
895 1886 {
896 - 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>';
897 1888 }
898 1889
899 1890 if ( $property->office_telephone_number != '' )
900 1891 {
901 - 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>';
902 1893 }
903 1894
904 1895 if ( $property->office_email_address != '' )
905 1896 {
906 - 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>';
907 1898 }
908 1899
909 1900 echo '</div>';
910 1901 }
@@ -957,12 +1948,12 @@
957 1948
958 1949 ob_start();
959 1950
960 1951 $api_key = get_option('propertyhive_google_maps_api_key', '');
961 - 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 );
962 1953 wp_enqueue_script('googlemaps');
963 1954
964 - 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>';
965 1956 ?>
966 1957 <script>
967 1958
968 1959 // We declare vars globally so developers can access them
@@ -983,8 +1974,9 @@
983 1974 $args['p'] = (int)$atts['office_id'];
984 1975 }
985 1976 else
986 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.
987 1979 $args['meta_query'] = array(
988 1980 array(
989 1981 'key' => 'primary',
990 1982 'value' => '1'
@@ -1012,11 +2004,11 @@
1012 2004 $lat = '51.509865';
1013 2005 $lng = '-0.118092';
1014 2006 }
1015 2007 ?>
1016 - 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; ?>);
1017 2009 var map_options = {
1018 - 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' ); ?>,
1019 2011 center: myLatlng,
1020 2012 mapTypeId: google.maps.MapTypeId.ROADMAP,
1021 2013 scrollwheel: <?php echo ( ( isset($atts['scrollwheel']) && ($atts['scrollwheel'] === 'false' || $atts['scrollwheel'] === FALSE) ) ? 'false' : 'true' ); ?>
1022 2014 }
@@ -1024,11 +2016,15 @@
1024 2016 if ( class_exists( 'PH_Map_Search' ) )
1025 2017 {
1026 2018 $map_add_on_settings = get_option( 'propertyhive_map_search', array() );
1027 2019
1028 - 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']) != '' )
1029 2021 {
1030 - 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 + }
1031 2027 }
1032 2028 }
1033 2029
1034 2030 do_action( 'propertyhive_office_map_options' );
@@ -1059,9 +2055,9 @@
1059 2055
1060 2056 if ( $lat != '' && $lng != '' )
1061 2057 {
1062 2058 ?>
1063 - 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; ?>);
1064 2060
1065 2061 var marker_options = {
1066 2062 map: office_map,
1067 2063 position: myLatlng,
@@ -1077,9 +2073,9 @@
1077 2073 {
1078 2074 $marker_icon_url = wp_get_attachment_url( $map_add_on_settings['custom_icon_attachment_id'] );
1079 2075 if ( $marker_icon_url !== FALSE )
1080 2076 {
1081 - echo 'marker_options.icon = \'' . $marker_icon_url . '\';';
2077 + echo 'marker_options.icon = \'' . esc_url($marker_icon_url) . '\';';
1082 2078 }
1083 2079 }
1084 2080 }
1085 2081 ?>
@@ -1150,9 +2146,9 @@
1150 2146 $form_controls = apply_filters( 'propertyhive_user_details_form_fields', $form_controls );
1151 2147
1152 2148 $form_controls_2 = ph_get_applicant_requirements_form_fields();
1153 2149
1154 - $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 );
1155 2151
1156 2152 $form_controls = array_merge( $form_controls, $form_controls_2 );
1157 2153
1158 2154 if ( get_option( 'propertyhive_applicant_registration_form_disclaimer', '' ) != '' )
@@ -1210,8 +2206,67 @@
1210 2206
1211 2207 }
1212 2208
1213 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 + /**
1214 2269 * Output 'My Account' page
1215 2270 *
1216 2271 * @param array $atts
1217 2272 * @return string
@@ -1244,6 +2299,59 @@
1244 2299 ph_get_template( 'account/my-account.php' );
1245 2300
1246 2301 return ob_get_clean();
1247 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 );
1248 2356 }
1249 2357 }