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 +1680 -104 1.4.62.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,16 +27,20 @@
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',
34 + 'office_map' => __CLASS__ . '::office_map',
30 35 'applicant_registration_form' => __CLASS__ . '::applicant_registration_form',
31 36 'propertyhive_my_account' => __CLASS__ . '::my_account',
32 37 'propertyhive_login_form' => __CLASS__ . '::login_form',
38 + 'propertyhive_reset_password_form' => __CLASS__ . '::reset_password_form',
33 39 );
34 40
35 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.
36 43 add_shortcode( apply_filters( "{$shortcode}_shortcode_tag", $shortcode ), $function );
37 44 }
38 45 }
39 46
@@ -57,10 +64,12 @@
57 64
58 65 $before = empty( $wrapper['before'] ) ? '<div class="' . esc_attr( $wrapper['class'] ) . '">' : $wrapper['before'];
59 66 $after = empty( $wrapper['after'] ) ? '</div>' : $wrapper['after'];
60 67
68 + // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Wrapper HTML is supplied by PHP callers; the default class is escaped when assembled.
61 69 echo $before;
62 70 call_user_func( $function, $atts );
71 + // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Closing wrapper HTML is supplied by PHP callers.
63 72 echo $after;
64 73
65 74 return ob_get_clean();
66 75 }
@@ -72,14 +81,16 @@
72 81 * @return string
73 82 */
74 83 public static function property_search_form( $atts ) {
75 84 $atts = shortcode_atts( array(
76 - 'id' => 'shortcode'
77 - ), $atts );
85 + 'id' => 'shortcode',
86 + 'default_department' => ''
87 + ), $atts, 'property_search_form' );
78 88
79 89 $form_controls = ph_get_search_form_fields();
80 90
81 - $form_controls = apply_filters( 'propertyhive_search_form_fields_' . $atts['id'], $form_controls );
91 + $form_controls = apply_filters( 'propertyhive_search_form_fields_' . $atts['id'], $form_controls, $atts );
92 + $form_controls = apply_filters( 'propertyhive_search_form_fields', $form_controls, $atts );
82 93
83 94 // We 100% need department so make sure it exists. If it doesn't, set a hidden field
84 95 if ( !isset($form_controls['department']) )
85 96 {
@@ -89,8 +100,20 @@
89 100
90 101 $form_controls['department'] = $original_department;
91 102 }
92 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 +
107 + if (
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.
110 + ( !isset($_REQUEST['department']) )
111 + )
112 + {
113 + $form_controls['department']['value'] = $atts['default_department'];
114 + }
115 +
93 116 ob_start();
94 117
95 118 ph_get_template( 'global/search-form.php', array( 'form_controls' => $form_controls, 'id' => $atts['id'] ) );
96 119
@@ -103,24 +126,72 @@
103 126 * @param array $atts
104 127 * @return string
105 128 */
106 129 public static function properties( $atts ) {
130 +
131 + global $propertyhive_loop;
132 +
107 133 $atts = shortcode_atts( array(
108 134 'columns' => '2',
109 135 'orderby' => 'meta_value_num',
110 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.
111 138 'meta_key' => '_price_actual',
112 139 'ids' => '',
113 - '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
143 + 'minimum_price' => '',
144 + 'maximum_price' => '',
114 145 'bedrooms' => '',
146 + 'minimum_bedrooms' => '',
147 + 'keyword' => '',
115 148 'address_keyword' => '',
116 - 'marketing_flag' => '', // Should be marketing_flag_id. Might deprecate this in the future
149 + 'country' => '',
150 + 'country_not' => '',
151 + 'availability_id' => '',
152 + 'marketing_flag' => '', // Deprecated. Use marketing_flag_id instead
153 + 'marketing_flag_id' => '', // Should be marketing_flag_id. Might deprecate this in the future
117 154 'property_type_id' => '',
155 + 'sale_by_id' => '',
118 156 'location_id' => '',
119 157 'office_id' => '',
120 - 'posts_per_page' => 10
121 - ), $atts );
158 + 'negotiator_id' => '',
159 + 'commercial_for_sale' => '',
160 + 'commercial_to_rent' => '',
161 + 'posts_per_page' => 10,
162 + 'no_results_output' => '',
163 + 'pagination' => '',
164 + 'show_order' => '',
165 + 'show_result_count' => '',
166 + 'carousel' => '',
167 + ), $atts, 'properties' );
122 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 +
123 194 $meta_query = array(
124 195 array(
125 196 'key' => '_on_market',
126 197 'value' => 'yes',
@@ -126,14 +197,18 @@
126 197 'value' => 'yes',
127 198 )
128 199 );
129 200
130 - 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() )) )
131 202 {
203 + $departments = explode(",", $atts['department']);
204 + $departments = array_map('trim', $departments);
205 + $departments = array_filter($departments);
206 +
132 207 $meta_query[] = array(
133 208 'key' => '_department',
134 - 'value' => $atts['department'],
135 - 'compare' => '='
209 + 'value' => $departments,
210 + 'compare' => 'IN'
136 211 );
137 212 }
138 213
139 214 if ( isset($atts['bedrooms']) && $atts['bedrooms'] != '' && is_numeric($atts['bedrooms']) )
@@ -140,15 +215,74 @@
140 215 {
141 216 $meta_query[] = array(
142 217 'key' => '_bedrooms',
143 218 'value' => sanitize_text_field( $atts['bedrooms'] ),
144 - 'compare' => '='
219 + 'compare' => '=',
220 + 'type' => 'NUMERIC'
145 221 );
146 222 }
147 223
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'] != '' )
241 + {
242 + $search_form_currency = get_option( 'propertyhive_search_form_currency', 'GBP' );
243 +
244 + $minimum_price = $atts['minimum_price'];
245 + if ( $search_form_currency != 'GBP' )
246 + {
247 + // Convert $atts['minimum_price'] to GBP
248 + $ph_countries = new PH_Countries();
249 +
250 + $minimum_price = $ph_countries->convert_price_to_gbp( $minimum_price, $search_form_currency );
251 + }
252 +
253 + $meta_query[] = array(
254 + 'key' => '_price_actual',
255 + 'value' => sanitize_text_field( floor( $minimum_price ) ),
256 + 'compare' => '>=',
257 + 'type' => 'NUMERIC'
258 + );
259 + }
260 +
261 + if ( isset($atts['department']) && ( $base_department == 'residential-sales' || $base_department == 'residential-lettings' ) && isset($atts['maximum_price']) && $atts['maximum_price'] != '' )
262 + {
263 + $search_form_currency = get_option( 'propertyhive_search_form_currency', 'GBP' );
264 +
265 + $maximum_price = $atts['maximum_price'];
266 + if ( $search_form_currency != 'GBP' )
267 + {
268 + // Convert $atts['maximum_price'] to GBP
269 + $ph_countries = new PH_Countries();
270 +
271 + $maximum_price = $ph_countries->convert_price_to_gbp( $maximum_price, $search_form_currency );
272 + }
273 +
274 + $meta_query[] = array(
275 + 'key' => '_price_actual',
276 + 'value' => sanitize_text_field( ceil( $maximum_price ) ),
277 + 'compare' => '<=',
278 + 'type' => 'NUMERIC'
279 + );
280 + }
281 +
148 282 if ( isset($atts['address_keyword']) && $atts['address_keyword'] != '' )
149 283 {
150 - $atts['address_keyword'] = sanitize_text_field( trim( $atts['address_keyword'] ) );
284 + $atts['address_keyword'] = ph_clean( trim( $atts['address_keyword'] ) );
151 285
152 286 $address_keywords = array( $atts['address_keyword'] );
153 287
154 288 if ( strpos( $atts['address_keyword'], ' ' ) !== FALSE )
@@ -158,37 +292,55 @@
158 292 if ( strpos( $atts['address_keyword'], '-' ) !== FALSE )
159 293 {
160 294 $address_keywords[] = str_replace("-", " ", $atts['address_keyword']);
161 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 + }
162 308
163 309 $sub_meta_query = array('relation' => 'OR');
164 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 +
165 317 foreach ( $address_keywords as $address_keyword )
166 318 {
167 319 $sub_meta_query[] = array(
168 320 'key' => '_reference_number',
169 321 'value' => $address_keyword,
170 - 'compare' => get_option( 'propertyhive_address_keyword_compare', '=' )
322 + 'compare' => $address_keyword_compare
171 323 );
172 324 $sub_meta_query[] = array(
173 325 'key' => '_address_street',
174 326 'value' => $address_keyword,
175 - 'compare' => get_option( 'propertyhive_address_keyword_compare', '=' )
327 + 'compare' => $address_keyword_compare
176 328 );
177 329 $sub_meta_query[] = array(
178 330 'key' => '_address_two',
179 331 'value' => $address_keyword,
180 - 'compare' => get_option( 'propertyhive_address_keyword_compare', '=' )
332 + 'compare' => $address_keyword_compare
181 333 );
182 334 $sub_meta_query[] = array(
183 335 'key' => '_address_three',
184 336 'value' => $address_keyword,
185 - 'compare' => get_option( 'propertyhive_address_keyword_compare', '=' )
337 + 'compare' => $address_keyword_compare
186 338 );
187 339 $sub_meta_query[] = array(
188 340 'key' => '_address_four',
189 341 'value' => $address_keyword,
190 - 'compare' => get_option( 'propertyhive_address_keyword_compare', '=' )
342 + 'compare' => $address_keyword_compare
191 343 );
192 344 }
193 345 if ( strlen($atts['address_keyword']) <= 4 )
194 346 {
@@ -196,11 +348,14 @@
196 348 'key' => '_address_postcode',
197 349 'value' => sanitize_text_field( $atts['address_keyword'] ),
198 350 'compare' => '='
199 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 [ ]
200 355 $sub_meta_query[] = array(
201 356 'key' => '_address_postcode',
202 - 'value' => sanitize_text_field( $atts['address_keyword'] ) . '[ ]',
357 + 'value' => sanitize_text_field( $atts['address_keyword'] ) . '[a-zA-Z]?[ ]',
203 358 'compare' => 'RLIKE'
204 359 );
205 360 }
206 361 else
@@ -214,50 +369,260 @@
214 369
215 370 $meta_query[] = $sub_meta_query;
216 371 }
217 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 +
218 500 if ( isset($atts['office_id']) && $atts['office_id'] != '' )
219 501 {
220 - $office_ids = explode(",", $atts['office_id']);
502 + $meta_query[] = array(
503 + 'key' => '_office_id',
504 + 'value' => explode(",", $atts['office_id']),
505 + 'compare' => 'IN',
506 + );
507 + }
221 508
509 + if ( isset($atts['negotiator_id']) && $atts['negotiator_id'] != '' )
510 + {
222 511 $meta_query[] = array(
223 - 'key' => '_office_id',
224 - 'value' => $office_ids,
225 - 'compare' => 'IN'
512 + 'key' => '_negotiator_id',
513 + 'value' => explode(",", $atts['negotiator_id']),
514 + 'compare' => 'IN',
226 515 );
227 516 }
228 517
518 + if ( isset($atts['commercial_for_sale']) && $atts['commercial_for_sale'] != '' )
519 + {
520 + $meta_query[] = array(
521 + 'key' => '_for_sale',
522 + 'value' => 'yes',
523 + 'compare' => '=',
524 + );
525 + }
526 +
527 + if ( isset($atts['commercial_to_rent']) && $atts['commercial_to_rent'] != '' )
528 + {
529 + $meta_query[] = array(
530 + 'key' => '_to_rent',
531 + 'value' => 'yes',
532 + 'compare' => '=',
533 + );
534 + }
535 +
229 536 $tax_query = array();
230 537
538 + if ( isset($atts['availability_id']) && $atts['availability_id'] != '' )
539 + {
540 + $tax_query[] = array(
541 + 'taxonomy' => 'availability',
542 + 'terms' => explode(",", $atts['availability_id']),
543 + 'compare' => 'IN',
544 + );
545 + }
546 +
547 + // Fallback for deprecated marketing_flag
231 548 if ( isset($atts['marketing_flag']) && $atts['marketing_flag'] != '' )
232 549 {
550 + $atts['marketing_flag_id'] = $atts['marketing_flag'];
551 + }
552 + if ( isset($atts['marketing_flag_id']) && $atts['marketing_flag_id'] != '' )
553 + {
233 554 $tax_query[] = array(
234 555 'taxonomy' => 'marketing_flag',
235 - 'terms' => array( $atts['marketing_flag'] )
556 + 'terms' => explode(",", $atts['marketing_flag_id']),
557 + 'compare' => 'IN',
236 558 );
237 559 }
238 560
239 561 if ( isset($atts['property_type_id']) && $atts['property_type_id'] != '' )
240 562 {
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 + )
573 + {
574 + $tax_query[] = array(
575 + 'taxonomy' => 'commercial_property_type',
576 + 'terms' => explode(",", $atts['property_type_id']),
577 + 'compare' => 'IN',
578 + );
579 + }
580 + else
581 + {
582 + $tax_query[] = array(
583 + 'taxonomy' => 'property_type',
584 + 'terms' => explode(",", $atts['property_type_id']),
585 + 'compare' => 'IN',
586 + );
587 + }
588 + }
589 +
590 + if ( isset($atts['location_id']) && $atts['location_id'] != '' )
591 + {
241 592 $tax_query[] = array(
242 - 'taxonomy' => 'property_type',
243 - 'terms' => array( $atts['property_type_id'] )
593 + 'taxonomy' => 'location',
594 + 'terms' => explode(",", $atts['location_id']),
595 + 'compare' => 'IN',
244 596 );
245 597 }
246 598
247 - if ( isset($atts['location_id']) && $atts['location_id'] != '' )
599 + if ( isset($atts['sale_by_id']) && $atts['sale_by_id'] != '' )
248 600 {
249 601 $tax_query[] = array(
250 - 'taxonomy' => 'location',
251 - 'terms' => array( $atts['location_id'] )
602 + 'taxonomy' => 'sale_by',
603 + 'terms' => explode(",", $atts['sale_by_id']),
604 + 'compare' => 'IN',
252 605 );
253 606 }
254 607
255 - if ( isset($atts['department']) && $atts['department'] == 'commercial' )
608 + // Change default meta key when department is specified as commercial, or if commercial is the only active department
609 + if (
610 + ( isset($atts['department']) && $base_department == 'commercial' ) ||
611 + (
612 + get_option( 'propertyhive_active_departments_sales' ) != 'yes' &&
613 + get_option( 'propertyhive_active_departments_lettings' ) != 'yes' &&
614 + get_option( 'propertyhive_active_departments_commercial' ) == 'yes'
615 + )
616 + )
256 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.
257 619 $atts['meta_key'] = '_floor_area_from_sqft';
258 620 }
259 621
622 + // Get which page we're currently viewing from the URL
623 + $paged = max( 1, get_query_var( 'paged' ) );
624 +
260 625 $args = array(
261 626 'post_type' => 'property',
262 627 'post_status' => ( ( is_user_logged_in() && current_user_can( 'manage_propertyhive' ) ) ? array('publish', 'private') : 'publish' ),
263 628 'ignore_sticky_posts' => 1,
@@ -263,28 +628,92 @@
263 628 'ignore_sticky_posts' => 1,
264 629 'orderby' => $atts['orderby'],
265 630 'order' => $atts['order'],
266 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.
267 634 'meta_query' => $meta_query,
268 - '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,
269 638 );
270 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.
271 641 $args['meta_key'] = $atts['meta_key'];
272 642 }
273 643
274 - if ( ! empty( $atts['ids'] ) ) {
275 - $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 + }
276 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 + }
277 668
669 + $args['orderby'] .= ' post_title';
670 +
278 671 ob_start();
279 672
280 - $properties = new WP_Query( apply_filters( 'propertyhive_properties_query', $args, $atts ) );
673 + do_action('propertyhive_shortcode_properties_before_catalog_ordering', $atts);
281 674
282 - $propertyhive_loop['columns'] = $atts['columns'];
675 + if ( isset($atts['show_order']) && $atts['show_order'] != '' )
676 + {
677 + list( $args, $orderby ) = self::get_show_order_args( $atts, $args );
283 678
679 + propertyhive_catalog_ordering( $atts['department'], $orderby );
680 + }
681 +
682 + do_action('propertyhive_shortcode_properties_after_catalog_ordering', $atts);
683 +
684 + $args = apply_filters( 'propertyhive_properties_query', $args, $atts );
685 + $args = apply_filters( 'propertyhive_shortcode_properties_query', $args, $atts );
686 +
687 + $properties = new WP_Query( $args );
688 +
689 + if ( isset($atts['show_result_count']) && $atts['show_result_count'] != '' )
690 + {
691 + $total_posts = $properties->found_posts;
692 +
693 + $first = ( $atts['posts_per_page'] * $paged ) - $atts['posts_per_page'] + 1;
694 + $last = min( $total_posts, $atts['posts_per_page'] * $paged );
695 +
696 + propertyhive_result_count( $paged, $atts['posts_per_page'], $total_posts, $first, $last);
697 + }
698 +
699 + do_action('propertyhive_shortcode_properties_after_result_count', $atts);
700 +
701 + $propertyhive_loop['columns'] = (int)$atts['columns'];
702 +
284 703 if ( $properties->have_posts() ) : ?>
285 704
286 - <?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 + ?>
287 716
288 717 <?php while ( $properties->have_posts() ) : $properties->the_post(); ?>
289 718
290 719 <?php ph_get_template_part( 'content', 'property' ); ?>
@@ -292,13 +721,24 @@
292 721 <?php endwhile; // end of the loop. ?>
293 722
294 723 <?php propertyhive_property_loop_end(); ?>
295 724
725 + <?php else: ?>
726 +
727 + <p class="propertyhive-info no-results-message"><?php echo wp_kses_post($atts['no_results_output']); ?></p>
728 +
296 729 <?php endif;
297 730
731 + if ( isset($atts['pagination']) && $atts['pagination'] != '' )
732 + {
733 + propertyhive_pagination( $properties->max_num_pages );
734 + }
735 +
298 736 wp_reset_postdata();
299 737
300 - return '<div class="propertyhive propertyhive-properties-shortcode columns-' . $atts['columns'] . '">' . ob_get_clean() . '</div>';
738 + $shortcode_output = ob_get_clean();
739 +
740 + return apply_filters( 'propertyhive_properties_shortcode_output', '<div class="propertyhive propertyhive-properties-shortcode columns-' . (int)$atts['columns'] . '">' . $shortcode_output . '</div>', $shortcode_output );
301 741 }
302 742
303 743 /**
304 744 * Recent Properties shortcode
@@ -313,54 +753,272 @@
313 753 $atts = shortcode_atts( array(
314 754 'per_page' => '12',
315 755 'columns' => '4',
316 756 'department' => '',
757 + 'minimum_price' => '',
317 758 'office_id' => '',
759 + 'negotiator_id' => '',
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' => '',
318 769 'orderby' => 'date',
319 - 'order' => 'desc'
320 - ), $atts );
770 + 'order' => 'desc',
771 + 'no_results_output' => '',
772 + 'pagination' => '',
773 + 'show_order' => '',
774 + 'show_result_count' => '',
775 + 'carousel' => '',
776 + ), $atts, 'recent_properties' );
321 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 +
322 803 $meta_query = PH()->query->get_meta_query();
323 804
324 805 if ( isset($atts['department']) && $atts['department'] != '' )
325 806 {
807 + $departments = explode(",", $atts['department']);
808 + $departments = array_map('trim', $departments);
809 + $departments = array_filter($departments);
810 +
326 811 $meta_query[] = array(
327 812 'key' => '_department',
328 - 'value' => $atts['department'],
329 - 'compare' => '='
813 + 'value' => $departments,
814 + 'compare' => 'IN'
330 815 );
331 816 }
332 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 +
333 845 if ( isset($atts['office_id']) && $atts['office_id'] != '' )
334 846 {
335 - $office_ids = explode(",", $atts['office_id']);
336 -
337 847 $meta_query[] = array(
338 848 'key' => '_office_id',
339 - 'value' => $office_ids,
849 + 'value' => explode(",", $atts['office_id']),
340 850 'compare' => 'IN'
341 851 );
342 852 }
343 853
854 + if ( isset($atts['negotiator_id']) && $atts['negotiator_id'] != '' )
855 + {
856 + $meta_query[] = array(
857 + 'key' => '_negotiator_id',
858 + 'value' => explode(",", $atts['negotiator_id']),
859 + 'compare' => 'IN',
860 + );
861 + }
862 +
863 + if ( isset($atts['commercial_for_sale']) && $atts['commercial_for_sale'] != '' )
864 + {
865 + $meta_query[] = array(
866 + 'key' => '_for_sale',
867 + 'value' => 'yes',
868 + 'compare' => '=',
869 + );
870 + }
871 +
872 + if ( isset($atts['commercial_to_rent']) && $atts['commercial_to_rent'] != '' )
873 + {
874 + $meta_query[] = array(
875 + 'key' => '_to_rent',
876 + 'value' => 'yes',
877 + 'compare' => '=',
878 + );
879 + }
880 +
881 + $tax_query = array();
882 +
883 + if ( isset($atts['availability_id']) && $atts['availability_id'] != '' )
884 + {
885 + $tax_query[] = array(
886 + 'taxonomy' => 'availability',
887 + 'terms' => explode(",", $atts['availability_id']),
888 + 'compare' => 'IN',
889 + );
890 + }
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 +
344 951 $args = array(
345 952 'post_type' => 'property',
346 953 'post_status' => ( ( is_user_logged_in() && current_user_can( 'manage_propertyhive' ) ) ? array('publish', 'private') : 'publish' ),
347 954 'ignore_sticky_posts' => 1,
348 955 'posts_per_page' => $atts['per_page'],
956 + 'paged' => $paged,
349 957 'orderby' => $atts['orderby'],
350 958 'order' => $atts['order'],
351 - 'meta_query' => $meta_query
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.
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.
962 + 'tax_query' => $tax_query,
963 + 'has_password' => false,
352 964 );
353 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 +
354 985 ob_start();
355 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 +
356 994 $properties = new WP_Query( apply_filters( 'propertyhive_shortcode_recent_properties_query', $args, $atts ) );
357 995
358 - $propertyhive_loop['columns'] = $atts['columns'];
996 + if ( isset($atts['show_result_count']) && $atts['show_result_count'] != '' )
997 + {
998 + $total_posts = $properties->found_posts;
359 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 +
360 1008 if ( $properties->have_posts() ) : ?>
361 1009
362 - <?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 + ?>
363 1021
364 1022 <?php while ( $properties->have_posts() ) : $properties->the_post(); ?>
365 1023
366 1024 <?php ph_get_template_part( 'content', 'property-recent' ); ?>
@@ -368,13 +1026,25 @@
368 1026 <?php endwhile; // end of the loop. ?>
369 1027
370 1028 <?php propertyhive_property_loop_end(); ?>
371 1029
1030 + <?php else: ?>
1031 +
1032 + <p class="propertyhive-info no-results-message"><?php echo wp_kses_post($atts['no_results_output']); ?></p>
1033 +
372 1034 <?php endif;
373 1035
1036 + if ( isset($atts['pagination']) && $atts['pagination'] != '' )
1037 + {
1038 + propertyhive_pagination( $properties->max_num_pages );
1039 + }
1040 +
374 1041 wp_reset_postdata();
375 1042
376 - return '<div class="propertyhive propertyhive-recent-properties-shortcode columns-' . $atts['columns'] . '">' . ob_get_clean() . '</div>';
1043 + $shortcode_output = ob_get_clean();
1044 +
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 );
1046 +
377 1047 }
378 1048
379 1049 /**
380 1050 * Output featured properties
@@ -389,21 +1059,62 @@
389 1059 $atts = shortcode_atts( array(
390 1060 'per_page' => '12',
391 1061 'columns' => '4',
392 1062 'department' => '',
1063 + 'address_keyword' => '',
393 1064 'office_id' => '',
1065 + 'negotiator_id' => '',
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' => '',
394 1069 'orderby' => 'rand',
395 1070 'order' => 'desc',
1071 + // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- Shortcode default declaration only; no query executes here.
396 1072 'meta_key' => '',
397 - ), $atts );
1073 + 'no_results_output' => '',
1074 + 'pagination' => '',
1075 + 'show_order' => '',
1076 + 'show_result_count' => '',
1077 + 'carousel' => '',
1078 + ), $atts, 'featured_properties' );
398 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 +
399 1108 $args = array(
400 1109 'post_type' => 'property',
401 1110 'post_status' => ( ( is_user_logged_in() && current_user_can( 'manage_propertyhive' ) ) ? array('publish', 'private') : 'publish' ),
402 1111 'ignore_sticky_posts' => 1,
403 1112 'posts_per_page' => $atts['per_page'],
1113 + 'paged' => $paged,
404 1114 'orderby' => $atts['orderby'],
405 1115 'order' => $atts['order'],
1116 + 'has_password' => false,
406 1117 );
407 1118
408 1119 $meta_query = array(
409 1120 array(
@@ -417,41 +1128,195 @@
417 1128 );
418 1129
419 1130 if ( isset($atts['department']) && $atts['department'] != '' )
420 1131 {
1132 + $departments = explode(",", $atts['department']);
1133 + $departments = array_map('trim', $departments);
1134 + $departments = array_filter($departments);
1135 +
421 1136 $meta_query[] = array(
422 1137 'key' => '_department',
423 - 'value' => $atts['department'],
424 - 'compare' => '='
1138 + 'value' => $departments,
1139 + 'compare' => 'IN'
425 1140 );
426 1141 }
427 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 +
428 1222 if ( isset($atts['office_id']) && $atts['office_id'] != '' )
429 1223 {
430 - $office_ids = explode(",", $atts['office_id']);
431 -
432 1224 $meta_query[] = array(
433 1225 'key' => '_office_id',
434 - 'value' => $office_ids,
1226 + 'value' => explode(",", $atts['office_id']),
435 1227 'compare' => 'IN'
436 1228 );
437 1229 }
438 1230
1231 + if ( isset($atts['negotiator_id']) && $atts['negotiator_id'] != '' )
1232 + {
1233 + $meta_query[] = array(
1234 + 'key' => '_negotiator_id',
1235 + 'value' => explode(",", $atts['negotiator_id']),
1236 + 'compare' => 'IN',
1237 + );
1238 + }
1239 +
1240 + // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- Property eligibility and matching fields live in the established metadata schema; retain these filters and the shortcode page limit.
439 1241 $args['meta_query'] = $meta_query;
440 1242
441 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.
442 1245 $args['meta_key'] = $atts['meta_key'];
443 1246 }
444 1247
1248 + $tax_query = array();
1249 +
1250 + if ( isset($atts['availability_id']) && $atts['availability_id'] != '' )
1251 + {
1252 + $tax_query[] = array(
1253 + 'taxonomy' => 'availability',
1254 + 'terms' => explode(",", $atts['availability_id']),
1255 + 'compare' => 'IN',
1256 + );
1257 + }
1258 +
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.
1261 + $args['tax_query'] = $tax_query;
1262 + }
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 +
445 1283 ob_start();
446 1284
1285 + if ( isset($atts['show_order']) && $atts['show_order'] != '' )
1286 + {
1287 + list( $args, $orderby ) = self::get_show_order_args( $atts, $args );
1288 +
1289 + propertyhive_catalog_ordering( $atts['department'], $orderby );
1290 + }
1291 +
447 1292 $properties = new WP_Query( apply_filters( 'propertyhive_shortcode_featured_properties_query', $args, $atts ) );
448 1293
449 - $propertyhive_loop['columns'] = $atts['columns'];
1294 + if ( isset($atts['show_result_count']) && $atts['show_result_count'] != '' )
1295 + {
1296 + $total_posts = $properties->found_posts;
450 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 +
451 1306 if ( $properties->have_posts() ) : ?>
452 1307
453 - <?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 + ?>
454 1319
455 1320 <?php while ( $properties->have_posts() ) : $properties->the_post(); ?>
456 1321
457 1322 <?php ph_get_template_part( 'content', 'property-featured' ); ?>
@@ -459,13 +1324,24 @@
459 1324 <?php endwhile; // end of the loop. ?>
460 1325
461 1326 <?php propertyhive_property_loop_end(); ?>
462 1327
1328 + <?php else: ?>
1329 +
1330 + <p class="propertyhive-info no-results-message"><?php echo wp_kses_post($atts['no_results_output']); ?></p>
1331 +
463 1332 <?php endif;
464 1333
1334 + if ( isset($atts['pagination']) && $atts['pagination'] != '' )
1335 + {
1336 + propertyhive_pagination( $properties->max_num_pages );
1337 + }
1338 +
465 1339 wp_reset_postdata();
466 1340
467 - return '<div class="propertyhive propertyhive-featured-properties-shortcode columns-' . $atts['columns'] . '">' . ob_get_clean() . '</div>';
1341 + $shortcode_output = ob_get_clean();
1342 +
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 );
468 1344 }
469 1345
470 1346 /**
471 1347 * Output similar properties
@@ -474,8 +1350,10 @@
474 1350 * @return string
475 1351 */
476 1352 public static function similar_properties( $atts ) {
477 1353
1354 + global $property, $propertyhive_loop;
1355 +
478 1356 $atts = shortcode_atts( array(
479 1357 'per_page' => '2',
480 1358 'columns' => '2',
481 1359 'orderby' => 'rand',
@@ -480,27 +1358,57 @@
480 1358 'columns' => '2',
481 1359 'orderby' => 'rand',
482 1360 'order' => 'asc',
483 1361 'price_percentage_bounds' => 10,
1362 + 'bedroom_bounds' => 0,
1363 + 'matching_address_field' => '', // only return fields with matching address field. Options: address_two, address_three, address_four, location
484 1364 'property_id' => '',
485 - ), $atts );
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' => '',
1370 + 'no_results_output' => '',
1371 + 'carousel' => '',
1372 + ), $atts, 'similar_properties' );
486 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 +
487 1404 if ($atts['property_id'] != '')
488 1405 {
489 1406 $department = get_post_meta( $atts['property_id'], '_department', true );
490 - $price = get_post_meta( $atts['property_id'], '_price_actual', true );
491 - $lower_price = $price;
492 - $higher_price = $price;
493 - $atts['price_percentage_bounds'] = str_replace("%", "", $atts['price_percentage_bounds']);
494 - if ( isset($atts['price_percentage_bounds']) && $atts['price_percentage_bounds'] != '' && is_numeric($atts['price_percentage_bounds']) && $atts['price_percentage_bounds'] > 0 )
495 - {
496 - $lower_price = $price - ($price * $atts['price_percentage_bounds'] / 100);
497 - $higher_price = $price + ($price * $atts['price_percentage_bounds'] / 100);
498 - }
499 - $bedrooms = get_post_meta( $atts['property_id'], '_bedrooms', true );
500 1407
501 1408 $args = array(
502 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.
503 1411 'post__not_in' => array($atts['property_id']),
504 1412 'post_status' => ( ( is_user_logged_in() && current_user_can( 'manage_propertyhive' ) ) ? array('publish', 'private') : 'publish' ),
505 1413 'ignore_sticky_posts' => 1,
506 1414 'posts_per_page' => $atts['per_page'],
@@ -505,8 +1413,9 @@
505 1413 'ignore_sticky_posts' => 1,
506 1414 'posts_per_page' => $atts['per_page'],
507 1415 'orderby' => $atts['orderby'],
508 1416 'order' => $atts['order'],
1417 + 'has_password' => false,
509 1418 );
510 1419
511 1420 $meta_query = array();
512 1421
@@ -519,39 +1428,327 @@
519 1428 'key' => '_on_market',
520 1429 'value' => 'yes',
521 1430 );
522 1431
523 - $meta_query[] = array(
524 - 'key' => '_bedrooms',
525 - 'value' => $bedrooms,
526 - 'type' => 'NUMERIC'
527 - );
1432 + if ( $department != 'commercial' && ph_get_custom_department_based_on( $department ) != 'commercial' )
1433 + {
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 + }
528 1443
529 - $meta_query[] = array(
530 - 'key' => '_price_actual',
531 - 'value' => $lower_price,
532 - 'compare' => '>=',
533 - 'type' => 'NUMERIC'
534 - );
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 + }
535 1453
536 - $meta_query[] = array(
537 - 'key' => '_price_actual',
538 - 'value' => $higher_price,
539 - 'compare' => '<=',
540 - 'type' => 'NUMERIC'
541 - );
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 + }
542 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 + }
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 );
1479 +
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' )) )
1632 + {
1633 + $address_field = get_post_meta( $atts['property_id'], '_' . $atts['matching_address_field'], true );
1634 +
1635 + $meta_query[] = array(
1636 + 'key' => '_' . $atts['matching_address_field'],
1637 + 'value' => $address_field,
1638 + );
1639 + }
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.
543 1642 $args['meta_query'] = $meta_query;
544 1643
1644 + $tax_query = array();
1645 +
1646 + if ( isset($atts['availability_id']) && $atts['availability_id'] != '' )
1647 + {
1648 + $tax_query[] = array(
1649 + 'taxonomy' => 'availability',
1650 + 'terms' => explode(",", $atts['availability_id']),
1651 + 'compare' => 'IN',
1652 + );
1653 + }
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 +
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.
1710 + $args['tax_query'] = $tax_query;
1711 + }
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 +
545 1732 ob_start();
546 1733
547 1734 $properties = new WP_Query( apply_filters( 'propertyhive_shortcode_similar_properties_query', $args, $atts ) );
548 1735
549 - $propertyhive_loop['columns'] = $atts['columns'];
1736 + $propertyhive_loop['columns'] = (int)$atts['columns'];
550 1737
551 1738 if ( $properties->have_posts() ) : ?>
552 1739
553 - <?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 + ?>
554 1751
555 1752 <?php while ( $properties->have_posts() ) : $properties->the_post(); ?>
556 1753
557 1754 <?php ph_get_template_part( 'content', 'property-featured' ); ?>
@@ -559,8 +1756,12 @@
559 1756 <?php endwhile; // end of the loop. ?>
560 1757
561 1758 <?php propertyhive_property_loop_end(); ?>
562 1759
1760 + <?php else: ?>
1761 +
1762 + <p class="propertyhive-info no-results-message"><?php echo wp_kses_post($atts['no_results_output']); ?></p>
1763 +
563 1764 <?php endif;
564 1765
565 1766 wp_reset_postdata();
566 1767 }
@@ -568,9 +1769,11 @@
568 1769 {
569 1770 echo 'No property_id passed into similar_properties shortcode';
570 1771 }
571 1772
572 - return '<div class="propertyhive propertyhive-similar-properties-shortcode columns-' . $atts['columns'] . '">' . ob_get_clean() . '</div>';
1773 + $shortcode_output = ob_get_clean();
1774 +
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 );
573 1776 }
574 1777
575 1778 /**
576 1779 * Output property map
@@ -583,21 +1786,49 @@
583 1786
584 1787 global $property;
585 1788
586 1789 $atts = shortcode_atts( array(
1790 + 'id' => '',
587 1791 'height' => '400',
588 1792 'zoom' => '14',
589 - 'scrollwheel' => 'true'
590 - ), $atts );
1793 + 'scrollwheel' => 'true',
1794 + 'init_on_load' => 'true',
1795 + 'embed' => 'false'
1796 + ), $atts, 'property_map' );
591 1797
592 1798 ob_start();
593 1799
594 - echo get_property_map( $atts );
1800 + get_property_map( $atts );
595 1801
596 1802 return ob_get_clean();
597 1803 }
598 1804
599 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 + /**
600 1831 * Output property street view
601 1832 * Should only be used on a property page or where the $property var is set
602 1833 *
603 1834 * @param array $atts
@@ -608,13 +1839,15 @@
608 1839 global $property;
609 1840
610 1841 $atts = shortcode_atts( array(
611 1842 'height' => '400',
612 - ), $atts );
1843 + 'init_on_load' => 'true',
1844 + 'embed' => 'false'
1845 + ), $atts, 'property_street_view' );
613 1846
614 1847 ob_start();
615 1848
616 - echo get_property_street_view( $atts );
1849 + get_property_street_view( $atts );
617 1850
618 1851 return ob_get_clean();
619 1852 }
620 1853
@@ -632,9 +1865,9 @@
632 1865 $atts = shortcode_atts( array(
633 1866 'address_separator' => '<br>',
634 1867 'hyperlink_telephone_number' => true,
635 1868 'hyperlink_email_address' => true,
636 - ), $atts );
1869 + ), $atts, 'property_office_details' );
637 1870
638 1871 $atts['hyperlink_telephone_number'] = (($atts['hyperlink_telephone_number'] === 'true' || $atts['hyperlink_telephone_number'] === true) ? true : false);
639 1872 $atts['hyperlink_email_address'] = (($atts['hyperlink_email_address'] === 'true' || $atts['hyperlink_email_address'] === true) ? true : false);
640 1873
@@ -645,24 +1878,24 @@
645 1878 echo '<div class="property-office-details">';
646 1879
647 1880 if ( $property->office_name != '' )
648 1881 {
649 - echo '<div class="office-name">' . $property->office_name . '</div>';
1882 + echo '<div class="office-name">' . esc_html($property->office_name) . '</div>';
650 1883 }
651 1884
652 1885 if ( $property->get_office_address( $atts['address_separator'] ) != '' )
653 1886 {
654 - 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>';
655 1888 }
656 1889
657 1890 if ( $property->office_telephone_number != '' )
658 1891 {
659 - 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>';
660 1893 }
661 1894
662 1895 if ( $property->office_email_address != '' )
663 1896 {
664 - 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>';
665 1898 }
666 1899
667 1900 echo '</div>';
668 1901 }
@@ -670,8 +1903,223 @@
670 1903 return ob_get_clean();
671 1904 }
672 1905
673 1906 /**
1907 + * Output office map
1908 + *
1909 + * @param array $atts
1910 + * @return string
1911 + */
1912 + public static function office_map( $atts ) {
1913 +
1914 + $offices_with_lat_lng = 0;
1915 + if ( !isset($atts['office_id']) || ( isset($atts['office_id']) && $atts['office_id'] == '' ) )
1916 + {
1917 + $args = array(
1918 + 'post_type' => 'office',
1919 + 'nopaging' => true
1920 + );
1921 +
1922 + $office_query = new WP_Query( $args );
1923 +
1924 + if ( $office_query->have_posts() )
1925 + {
1926 + while ( $office_query->have_posts() )
1927 + {
1928 + $office_query->the_post();
1929 +
1930 + $lat = get_post_meta(get_the_ID(), '_office_latitude', TRUE);
1931 + $lng = get_post_meta(get_the_ID(), '_office_longitude', TRUE);
1932 +
1933 + if ( $lat != '' && $lng != '' )
1934 + {
1935 + ++$offices_with_lat_lng;
1936 + }
1937 + }
1938 + }
1939 + wp_reset_postdata();
1940 + }
1941 +
1942 + $atts = shortcode_atts( array(
1943 + 'office_id' => '', // if wanting to show map for a particular office
1944 + 'height' => '400',
1945 + 'zoom' => ( ( ( !isset($atts['office_id']) || ( isset($atts['office_id']) && $atts['office_id'] == '' ) ) && !isset($atts['zoom']) && $offices_with_lat_lng > 1 ) ? 'auto' : '14' ),
1946 + 'scrollwheel' => 'true'
1947 + ), $atts, 'office_map' );
1948 +
1949 + ob_start();
1950 +
1951 + $api_key = get_option('propertyhive_google_maps_api_key', '');
1952 + wp_register_script('googlemaps', '//maps.googleapis.com/maps/api/js?' . ( ( $api_key != '' && $api_key !== FALSE ) ? 'key=' . $api_key : '' ), false, '3', true );
1953 + wp_enqueue_script('googlemaps');
1954 +
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>';
1956 +?>
1957 +<script>
1958 +
1959 + // We declare vars globally so developers can access them
1960 + var office_map; // Global declaration of the map
1961 + var office_marker;
1962 + var ph_office_map_lat_lngs = new Array();
1963 +
1964 + function initialize_office_map() {
1965 +
1966 + <?php
1967 + $args = array(
1968 + 'post_type' => 'office',
1969 + 'posts_per_page' => 1
1970 + );
1971 +
1972 + if ( isset($atts['office_id']) && (int)$atts['office_id'] != 0 )
1973 + {
1974 + $args['p'] = (int)$atts['office_id'];
1975 + }
1976 + else
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.
1979 + $args['meta_query'] = array(
1980 + array(
1981 + 'key' => 'primary',
1982 + 'value' => '1'
1983 + )
1984 + );
1985 + }
1986 +
1987 + $office_query = new WP_Query( $args );
1988 +
1989 + $lat = '';
1990 + $lng = '';
1991 +
1992 + if ( $office_query->have_posts() )
1993 + {
1994 + while ( $office_query->have_posts() )
1995 + {
1996 + $office_query->the_post();
1997 +
1998 + $lat = get_post_meta(get_the_ID(), '_office_latitude', TRUE);
1999 + $lng = get_post_meta(get_the_ID(), '_office_longitude', TRUE);
2000 + }
2001 + }
2002 + if ( $lat == '' || $lng == '' )
2003 + {
2004 + $lat = '51.509865';
2005 + $lng = '-0.118092';
2006 + }
2007 + ?>
2008 + var myLatlng = new google.maps.LatLng(<?php echo (float)$lat; ?>, <?php echo (float)$lng; ?>);
2009 + var map_options = {
2010 + zoom: <?php echo ( ( isset($atts['zoom']) && !empty($atts['zoom']) && is_numeric($atts['zoom']) && $atts['zoom'] != 'auto' ) ? (int)$atts['zoom'] : '14' ); ?>,
2011 + center: myLatlng,
2012 + mapTypeId: google.maps.MapTypeId.ROADMAP,
2013 + scrollwheel: <?php echo ( ( isset($atts['scrollwheel']) && ($atts['scrollwheel'] === 'false' || $atts['scrollwheel'] === FALSE) ) ? 'false' : 'true' ); ?>
2014 + }
2015 + <?php
2016 + if ( class_exists( 'PH_Map_Search' ) )
2017 + {
2018 + $map_add_on_settings = get_option( 'propertyhive_map_search', array() );
2019 +
2020 + if ( isset($map_add_on_settings['style_js']) && is_string($map_add_on_settings['style_js']) && trim($map_add_on_settings['style_js']) != '' )
2021 + {
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 + }
2027 + }
2028 + }
2029 +
2030 + do_action( 'propertyhive_office_map_options' );
2031 + ?>
2032 + office_map = new google.maps.Map(document.getElementById("office_map_canvas"), map_options);
2033 +
2034 + <?php
2035 + $args = array(
2036 + 'post_type' => 'office',
2037 + 'nopaging' => true
2038 + );
2039 +
2040 + if ( isset($atts['office_id']) && (int)$atts['office_id'] != 0 )
2041 + {
2042 + $args['p'] = (int)$atts['office_id'];
2043 + }
2044 +
2045 + $office_query = new WP_Query( $args );
2046 +
2047 + if ( $office_query->have_posts() )
2048 + {
2049 + while ( $office_query->have_posts() )
2050 + {
2051 + $office_query->the_post();
2052 +
2053 + $lat = get_post_meta(get_the_ID(), '_office_latitude', TRUE);
2054 + $lng = get_post_meta(get_the_ID(), '_office_longitude', TRUE);
2055 +
2056 + if ( $lat != '' && $lng != '' )
2057 + {
2058 + ?>
2059 + var myLatlng = new google.maps.LatLng(<?php echo (float)$lat; ?>, <?php echo (float)$lng; ?>);
2060 +
2061 + var marker_options = {
2062 + map: office_map,
2063 + position: myLatlng,
2064 + title: "<?php echo esc_attr(get_the_title()); ?>"
2065 + };
2066 +
2067 + <?php
2068 + if ( class_exists( 'PH_Map_Search' ) )
2069 + {
2070 + $map_add_on_settings = get_option( 'propertyhive_map_search', array() );
2071 +
2072 + if ( isset($map_add_on_settings['icon_type']) && $map_add_on_settings['icon_type'] == 'custom_single' && isset($map_add_on_settings['custom_icon_attachment_id']) && $map_add_on_settings['custom_icon_attachment_id'] != '' )
2073 + {
2074 + $marker_icon_url = wp_get_attachment_url( $map_add_on_settings['custom_icon_attachment_id'] );
2075 + if ( $marker_icon_url !== FALSE )
2076 + {
2077 + echo 'marker_options.icon = \'' . esc_url($marker_icon_url) . '\';';
2078 + }
2079 + }
2080 + }
2081 + ?>
2082 +
2083 + <?php do_action( 'propertyhive_office_map_marker_options' ); ?>
2084 +
2085 + office_marker = new google.maps.Marker(marker_options);
2086 + ph_office_map_lat_lngs.push(office_marker.getPosition());
2087 + <?php
2088 + }
2089 + }
2090 + }
2091 + wp_reset_postdata();
2092 +
2093 + if ( $atts['zoom'] == 'auto' ) { echo 'ph_fit_office_map_to_bounds();'; }
2094 + ?>
2095 + }
2096 +
2097 + function ph_fit_office_map_to_bounds()
2098 + {
2099 + var bounds = new google.maps.LatLngBounds();
2100 + if ( ph_office_map_lat_lngs.length > 0 )
2101 + {
2102 + for ( var i = 0; i < ph_office_map_lat_lngs.length; i++ )
2103 + {
2104 + bounds.extend(ph_office_map_lat_lngs[i]);
2105 + }
2106 + office_map.fitBounds(bounds);
2107 + }
2108 + }
2109 +
2110 + if(window.addEventListener) {
2111 + window.addEventListener('load', initialize_office_map);
2112 + }else{
2113 + window.attachEvent('onload', initialize_office_map);
2114 + }
2115 +
2116 +</script>
2117 +<?php
2118 + return ob_get_clean();
2119 + }
2120 +
2121 + /**
674 2122 * Output applicant registration form
675 2123 *
676 2124 * @param array $atts
677 2125 * @return string
@@ -679,9 +2127,9 @@
679 2127 public static function applicant_registration_form( $atts ) {
680 2128
681 2129 $atts = shortcode_atts( array(
682 2130
683 - ), $atts );
2131 + ), $atts, 'applicant_registration_form' );
684 2132
685 2133 $assets_path = str_replace( array( 'http:', 'https:' ), '', PH()->plugin_url() ) . '/assets/';
686 2134 wp_enqueue_script( 'propertyhive_account', $assets_path . 'js/frontend/account.js', array( 'jquery' ), PH_VERSION, true );
687 2135
@@ -693,17 +2141,33 @@
693 2141 return ob_get_clean();
694 2142 }
695 2143
696 2144 $form_controls = ph_get_user_details_form_fields();
697 -
2145 +
698 2146 $form_controls = apply_filters( 'propertyhive_user_details_form_fields', $form_controls );
699 2147
700 2148 $form_controls_2 = ph_get_applicant_requirements_form_fields();
701 -
702 - $form_controls_2 = apply_filters( 'propertyhive_applicant_requirements_form_fields', $form_controls_2 );
703 2149
704 - ph_get_template( 'account/applicant-registration-form.php', array( 'form_controls' => array_merge( $form_controls, $form_controls_2 ) ) );
2150 + $form_controls_2 = apply_filters( 'propertyhive_applicant_requirements_form_fields', $form_controls_2, false );
705 2151
2152 + $form_controls = array_merge( $form_controls, $form_controls_2 );
2153 +
2154 + if ( get_option( 'propertyhive_applicant_registration_form_disclaimer', '' ) != '' )
2155 + {
2156 + $disclaimer = get_option( 'propertyhive_applicant_registration_form_disclaimer', '' );
2157 +
2158 + $form_controls['disclaimer'] = array(
2159 + 'type' => 'checkbox',
2160 + 'label' => $disclaimer,
2161 + 'label_style' => 'width:100%;',
2162 + 'required' => true
2163 + );
2164 + }
2165 +
2166 + $form_controls = apply_filters( 'propertyhive_applicant_registration_form_fields', $form_controls );
2167 +
2168 + ph_get_template( 'account/applicant-registration-form.php', array( 'form_controls' => $form_controls ) );
2169 +
706 2170 return ob_get_clean();
707 2171 }
708 2172
709 2173 /**
@@ -715,9 +2179,9 @@
715 2179 public static function login_form( $atts )
716 2180 {
717 2181 $atts = shortcode_atts( array(
718 2182
719 - ), $atts );
2183 + ), $atts, 'login_form' );
720 2184
721 2185 $assets_path = str_replace( array( 'http:', 'https:' ), '', PH()->plugin_url() ) . '/assets/';
722 2186 wp_enqueue_script( 'propertyhive_account', $assets_path . 'js/frontend/account.js', array( 'jquery' ), PH_VERSION, true );
723 2187
@@ -742,8 +2206,67 @@
742 2206
743 2207 }
744 2208
745 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 + /**
746 2269 * Output 'My Account' page
747 2270 *
748 2271 * @param array $atts
749 2272 * @return string
@@ -751,9 +2274,9 @@
751 2274 public static function my_account( $atts )
752 2275 {
753 2276 $atts = shortcode_atts( array(
754 2277
755 - ), $atts );
2278 + ), $atts, 'my_account' );
756 2279
757 2280 $assets_path = str_replace( array( 'http:', 'https:' ), '', PH()->plugin_url() ) . '/assets/';
758 2281 wp_enqueue_script( 'propertyhive_account', $assets_path . 'js/frontend/account.js', array( 'jquery' ), PH_VERSION, true );
759 2282
@@ -776,6 +2299,59 @@
776 2299 ph_get_template( 'account/my-account.php' );
777 2300
778 2301 return ob_get_clean();
779 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 );
780 2356 }
781 2357 }