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 +244 -5 1.4.62.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,16 +17,152 @@
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 - return is_array( $var ) ? array_map( 'ph_clean', $var ) : sanitize_text_field( $var );
111 +
112 + if ( is_array( $var ) ) {
113 + return array_map( 'ph_clean', $var );
114 + } else {
115 + return is_scalar( $var ) ? sanitize_text_field( $var ) : $var;
116 + }
24 117 }
25 118
119 +/**
120 + * Strip none numeric or comma chars from phone numbers
121 + * @param string
122 + * @return string
123 + */
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.
126 +function ph_clean_telephone_number( $var ) {
127 +
128 + return preg_replace( "/[^0-9,]/", "", $var );
129 +}
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 +
26 165 if ( ! function_exists( 'ph_rgb_from_hex' ) ) {
27 166
28 167 /**
29 168 * Hex darker/lighter/contrast functions for colours.
@@ -30,8 +169,9 @@
30 169 *
31 170 * @param mixed $color
32 171 * @return string
33 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.
34 174 function ph_rgb_from_hex( $color ) {
35 175 $color = str_replace( '#', '', $color );
36 176 // Convert shorthand colors to full format, e.g. "FFF" -> "FFFFFF"
37 177 $color = preg_replace( '~^(.)(.)(.)$~', '$1$1$2$2$3$3', $color );
@@ -36,11 +176,11 @@
36 176 // Convert shorthand colors to full format, e.g. "FFF" -> "FFFFFF"
37 177 $color = preg_replace( '~^(.)(.)(.)$~', '$1$1$2$2$3$3', $color );
38 178
39 179 $rgb = array();
40 - $rgb['R'] = hexdec( $color{0} . $color{1} );
41 - $rgb['G'] = hexdec( $color{2} . $color{3} );
42 - $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] );
43 183
44 184 return $rgb;
45 185 }
46 186 }
@@ -53,8 +193,9 @@
53 193 * @param mixed $color
54 194 * @param int $factor (default: 30)
55 195 * @return string
56 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.
57 198 function ph_hex_darker( $color, $factor = 30 ) {
58 199 $base = ph_rgb_from_hex( $color );
59 200 $color = '#';
60 201
@@ -82,8 +223,9 @@
82 223 * @param mixed $color
83 224 * @param int $factor (default: 30)
84 225 * @return string
85 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.
86 228 function ph_hex_lighter( $color, $factor = 30 ) {
87 229 $base = ph_rgb_from_hex( $color );
88 230 $color = '#';
89 231
@@ -113,8 +255,9 @@
113 255 * @param string $dark (default: '#000000')
114 256 * @param string $light (default: '#FFFFFF')
115 257 * @return string
116 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.
117 260 function ph_light_or_dark( $color, $dark = '#000000', $light = '#FFFFFF' ) {
118 261
119 262 $hex = str_replace( '#', '', $color );
120 263
@@ -135,8 +278,9 @@
135 278 *
136 279 * @param string $hex
137 280 * @return string
138 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.
139 283 function ph_format_hex( $hex ) {
140 284
141 285 $hex = trim( str_replace( '#', '', $hex ) );
142 286
@@ -145,5 +289,100 @@
145 289 }
146 290
147 291 return $hex ? '#' . $hex : null;
148 292 }
149 -}
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 +}