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/ph-formatting-functions.php +227 -4 1.4.542.3.0 View file →
@@ -1,5 +1,8 @@
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.
4 +
2 5 /**
3 6 * PropertyHive Formatting
4 7 *
5 8 * Functions for formatting data.
@@ -14,12 +17,97 @@
14 17 exit; // Exit if accessed directly
15 18 }
16 19
17 20 /**
21 + * Clean stored description HTML while retaining ordinary links and tour frames.
22 + */
23 +function propertyhive_sanitize_description( $html ) {
24 + $allowed = wp_kses_allowed_html( 'post' );
25 + $allowed['iframe'] = array_fill_keys( array( 'src', 'title', 'width', 'height', 'allow', 'allowfullscreen', 'frameborder', 'loading', 'referrerpolicy' ), true );
26 + $clean = wp_kses( $html, $allowed, wp_allowed_protocols() );
27 +
28 + // KSES attribute callbacks are unavailable on WordPress 5.6. Check only the
29 + // normalized iframe tags here, including quoted attributes containing >.
30 + return preg_replace_callback( '~<iframe\b(?:[^>"\']++|"[^"]*+"|\'[^\']*+\')*>~i', static function( $match ) {
31 + $attributes = wp_kses_hair( substr( $match[0], 7, -1 ), wp_allowed_protocols() );
32 + $sources = array();
33 + foreach ( $attributes as $name => $attribute ) {
34 + if ( strtolower( $name ) === 'src' ) {
35 + $sources[] = $attribute['value'];
36 + }
37 + }
38 + if ( empty( $sources ) ) {
39 + return $match[0];
40 + }
41 + if ( count( $sources ) !== 1 ) {
42 + return '<iframe>';
43 + }
44 + $url = html_entity_decode( $sources[0], ENT_QUOTES, 'UTF-8' );
45 + $parts = wp_parse_url( $url );
46 + if ( ! is_array( $parts ) || empty( $parts['host'] ) || ! preg_match( '~^(?:https?:)?//~i', $url ) || ( isset( $parts['scheme'] ) && ! in_array( strtolower( $parts['scheme'] ), array( 'http', 'https' ), true ) ) ) {
47 + return '<iframe>';
48 + }
49 + if ( ! apply_filters( 'propertyhive_description_iframe_url_allowed', true, $url ) ) {
50 + return '<iframe>';
51 + }
52 + return $match[0];
53 + }, $clean );
54 +}
55 +
56 +/**
57 + * Translate built-in rent frequency labels, preserving custom labels.
58 + *
59 + * @param string $frequency Stored frequency.
60 + * @return string Display label.
61 + */
62 +function propertyhive_get_rent_frequency_label( $frequency ) {
63 + $labels = array(
64 + 'pd' => __( 'pd', 'propertyhive' ),
65 + 'pppw' => __( 'pppw', 'propertyhive' ),
66 + 'pw' => __( 'pw', 'propertyhive' ),
67 + 'pcm' => __( 'pcm', 'propertyhive' ),
68 + 'pq' => __( 'pq', 'propertyhive' ),
69 + 'pa' => __( 'pa', 'propertyhive' ),
70 + );
71 + return isset( $labels[$frequency] ) ? $labels[$frequency] : $frequency;
72 +}
73 +
74 +/**
75 + * Translate built-in CRM statuses without passing dynamic text to gettext.
76 + *
77 + * @param string $status Stored status.
78 + * @return string Display label.
79 + */
80 +function propertyhive_get_status_label( $status ) {
81 + $labels = array(
82 + 'pending' => __( 'Pending', 'propertyhive' ),
83 + 'confirmed' => __( 'Confirmed', 'propertyhive' ),
84 + 'unconfirmed' => __( 'Unconfirmed', 'propertyhive' ),
85 + 'carried_out' => __( 'Carried Out', 'propertyhive' ),
86 + 'awaiting_feedback' => __( 'Awaiting Feedback', 'propertyhive' ),
87 + 'feedback_passed_on' => __( 'Feedback Passed On', 'propertyhive' ),
88 + 'feedback_not_passed_on' => __( 'Feedback Not Passed On', 'propertyhive' ),
89 + 'cancelled' => __( 'Cancelled', 'propertyhive' ),
90 + 'no_show' => __( 'No Show', 'propertyhive' ),
91 + 'offer_made' => __( 'Offer Made', 'propertyhive' ),
92 + 'accepted' => __( 'Accepted', 'propertyhive' ),
93 + 'declined' => __( 'Declined', 'propertyhive' ),
94 + 'current' => __( 'Current', 'propertyhive' ),
95 + 'exchanged' => __( 'Exchanged', 'propertyhive' ),
96 + 'completed' => __( 'Completed', 'propertyhive' ),
97 + 'fallen_through' => __( 'Fallen Through', 'propertyhive' ),
98 + 'interested' => __( 'Interested', 'propertyhive' ),
99 + 'not_interested' => __( 'Not Interested', 'propertyhive' ),
100 + );
101 + return isset( $labels[$status] ) ? $labels[$status] : ucwords( str_replace( '_', ' ', $status ) );
102 +}
103 +
104 +/**
18 105 * Clean variables using sanitize_text_field.
19 106 * @param string|array $var
20 107 * @return string|array
21 108 */
109 +// phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedFunctionFound -- Legacy public global helper ph_clean; the established callable name is part of the plugin/extension API and must remain stable.
22 110 function ph_clean( $var ) {
23 111
24 112 if ( is_array( $var ) ) {
25 113 return array_map( 'ph_clean', $var );
@@ -33,13 +121,48 @@
33 121 * @param string
34 122 * @return string
35 123 */
36 124
125 +// phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedFunctionFound -- Legacy public global helper ph_clean_telephone_number; the established callable name is part of the plugin/extension API and must remain stable.
37 126 function ph_clean_telephone_number( $var ) {
38 127
39 128 return preg_replace( "/[^0-9,]/", "", $var );
40 129 }
41 130
131 +/**
132 + * Format monetary number value with decimal and thousands separators for display in a form field
133 + * @param string
134 + * @return string
135 + */
136 +// phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedFunctionFound -- Legacy public global helper ph_display_price_field; the established callable name is part of the plugin/extension API and must remain stable.
137 +function ph_display_price_field( $var, $use_separator_setting = false )
138 +{
139 + $float_var = (float)$var;
140 +
141 + // If stored value isn't a valid number with decimals, display as it's stored
142 + if ( $float_var !== floatval(0) )
143 + {
144 + // If there are decimals on the number, display them. If not, display none
145 + $decimals = $float_var == intval($var) ? 0 : 2;
146 +
147 + if ( !$use_separator_setting )
148 + {
149 + $decimal_separator = '.';
150 + $thousands_separator = ',';
151 + }
152 + else
153 + {
154 + // Get custom thousands and decimal and separators, if set, when not displaying in an input
155 + $thousands_separator = get_option('propertyhive_price_thousand_separator', ',');
156 + $decimal_separator = get_option('propertyhive_price_decimal_separator', '.');
157 + }
158 +
159 + $var = number_format( $float_var, $decimals, $decimal_separator, $thousands_separator );
160 + }
161 +
162 + return (string)$var;
163 +}
164 +
42 165 if ( ! function_exists( 'ph_rgb_from_hex' ) ) {
43 166
44 167 /**
45 168 * Hex darker/lighter/contrast functions for colours.
@@ -46,8 +169,9 @@
46 169 *
47 170 * @param mixed $color
48 171 * @return string
49 172 */
173 + // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedFunctionFound -- Legacy public global helper ph_rgb_from_hex; the established callable name is part of the plugin/extension API and must remain stable.
50 174 function ph_rgb_from_hex( $color ) {
51 175 $color = str_replace( '#', '', $color );
52 176 // Convert shorthand colors to full format, e.g. "FFF" -> "FFFFFF"
53 177 $color = preg_replace( '~^(.)(.)(.)$~', '$1$1$2$2$3$3', $color );
@@ -52,11 +176,11 @@
52 176 // Convert shorthand colors to full format, e.g. "FFF" -> "FFFFFF"
53 177 $color = preg_replace( '~^(.)(.)(.)$~', '$1$1$2$2$3$3', $color );
54 178
55 179 $rgb = array();
56 - $rgb['R'] = hexdec( $color{0} . $color{1} );
57 - $rgb['G'] = hexdec( $color{2} . $color{3} );
58 - $rgb['B'] = hexdec( $color{4} . $color{5} );
180 + $rgb['R'] = hexdec( $color[0] . $color[1] );
181 + $rgb['G'] = hexdec( $color[2] . $color[3] );
182 + $rgb['B'] = hexdec( $color[4] . $color[5] );
59 183
60 184 return $rgb;
61 185 }
62 186 }
@@ -69,8 +193,9 @@
69 193 * @param mixed $color
70 194 * @param int $factor (default: 30)
71 195 * @return string
72 196 */
197 + // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedFunctionFound -- Legacy public global helper ph_hex_darker; the established callable name is part of the plugin/extension API and must remain stable.
73 198 function ph_hex_darker( $color, $factor = 30 ) {
74 199 $base = ph_rgb_from_hex( $color );
75 200 $color = '#';
76 201
@@ -98,8 +223,9 @@
98 223 * @param mixed $color
99 224 * @param int $factor (default: 30)
100 225 * @return string
101 226 */
227 + // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedFunctionFound -- Legacy public global helper ph_hex_lighter; the established callable name is part of the plugin/extension API and must remain stable.
102 228 function ph_hex_lighter( $color, $factor = 30 ) {
103 229 $base = ph_rgb_from_hex( $color );
104 230 $color = '#';
105 231
@@ -129,8 +255,9 @@
129 255 * @param string $dark (default: '#000000')
130 256 * @param string $light (default: '#FFFFFF')
131 257 * @return string
132 258 */
259 + // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedFunctionFound -- Legacy public global helper ph_light_or_dark; the established callable name is part of the plugin/extension API and must remain stable.
133 260 function ph_light_or_dark( $color, $dark = '#000000', $light = '#FFFFFF' ) {
134 261
135 262 $hex = str_replace( '#', '', $color );
136 263
@@ -151,8 +278,9 @@
151 278 *
152 279 * @param string $hex
153 280 * @return string
154 281 */
282 + // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedFunctionFound -- Legacy public global helper ph_format_hex; the established callable name is part of the plugin/extension API and must remain stable.
155 283 function ph_format_hex( $hex ) {
156 284
157 285 $hex = trim( str_replace( '#', '', $hex ) );
158 286
@@ -161,5 +289,100 @@
161 289 }
162 290
163 291 return $hex ? '#' . $hex : null;
164 292 }
165 -}
293 +}
294 +
295 +// phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedFunctionFound -- Legacy public global helper ph_nl2br; the established callable name is part of the plugin/extension API and must remain stable.
296 +function ph_nl2br($str)
297 +{
298 + // Match any <ul> or <ol> with their content
299 + $pattern = '/(<ul[^>]*>.*?<\/ul>|<ol[^>]*>.*?<\/ol>)/is';
300 + $parts = preg_split($pattern, $str, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY);
301 +
302 + foreach ($parts as &$part) {
303 + // If the part is not a list, apply nl2br
304 + if (!preg_match($pattern, $part)) {
305 + $part = nl2br($part);
306 + }
307 + }
308 +
309 + // Reassemble the string
310 + return implode('', $parts);
311 +}
312 +
313 +// phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedFunctionFound -- Legacy public global helper ph_split_address_into_fields; the established callable name is part of the plugin/extension API and must remain stable.
314 +function ph_split_address_into_fields( $address )
315 +{
316 + $fields = [
317 + 'address_name_number' => '',
318 + 'address_street' => '',
319 + 'address_two' => '',
320 + 'address_three' => '',
321 + 'address_four' => '',
322 + 'address_postcode' => '',
323 + 'address_country' => ''
324 + ];
325 +
326 + // Replace newlines with commas and remove consecutive commas
327 + $address = preg_replace('/\s*,\s*/', ', ', $address);
328 + $address = preg_replace('/\s*\n\s*/', ', ', $address);
329 + $address = preg_replace('/,+/', ',', $address);
330 +
331 + // Explode the address by comma
332 + $parts = explode(',', $address);
333 +
334 + // Trim whitespace from each part
335 + $parts = array_map('trim', $parts);
336 +
337 + // Remove empty parts
338 + $parts = array_filter($parts);
339 +
340 + // Check the first part for building number and street
341 + if (isset($parts[0])) {
342 + if (preg_match('/^(\d+)\s+(.*)$/', $parts[0], $matches)) {
343 + $fields['address_name_number'] = $matches[1];
344 + $fields['address_street'] = $matches[2];
345 + } else {
346 + $fields['address_street'] = $parts[0];
347 + }
348 + array_shift($parts);
349 + }
350 +
351 + // Detect postcode in the remaining parts
352 + foreach ($parts as $index => $part) {
353 + if (preg_match('/[A-Z]{1,2}\d[A-Z\d]? \d[A-Z]{2}/i', $part, $postcodeMatch)) {
354 + $fields['address_postcode'] = $postcodeMatch[0];
355 + // Split the part containing the postcode
356 + $remainingPart = preg_replace('/[A-Z]{1,2}\d[A-Z\d]? \d[A-Z]{2}/i', '', $part);
357 + if (!empty(trim($remainingPart))) {
358 + array_splice($parts, $index, 1, trim($remainingPart));
359 + } else {
360 + unset($parts[$index]);
361 + }
362 + // Check if the next part is the country
363 + if (isset($parts[$index + 1])) {
364 + $fields['address_country'] = $parts[$index + 1];
365 + unset($parts[$index + 1]);
366 + }
367 + break;
368 + }
369 + }
370 +
371 + // Assign remaining parts to Address Line 2, Town/City, and County
372 + $remainingParts = array_values($parts);
373 + if (isset($remainingParts[0])) $fields['address_two'] = $remainingParts[0];
374 + if (isset($remainingParts[1])) $fields['address_three'] = $remainingParts[1];
375 + if (isset($remainingParts[2])) $fields['address_four'] = $remainingParts[2];
376 +
377 + return $fields;
378 +}
379 +/**
380 + * Resolve translated built-in or extension-provided department labels.
381 + *
382 + * @param string $department Department key.
383 + * @return string Department label.
384 + */
385 +function propertyhive_get_department_label( $department ) {
386 + $labels = ph_get_departments();
387 + return isset( $labels[$department] ) ? $labels[$department] : ucwords( str_replace( '-', ' ', $department ) );
388 +}