PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.8
Yatra – Travel Booking & Tour Operator Software v3.0.8
3.0.15 3.0.14 3.0.14.1 3.0.14.2 3.0.12 3.0.13 3.0.11 3.0.10 3.0.9 3.0.8 3.0.7 3.0.6 3.0.5 3.0.5.1 3.0.4 3.0.3 3.0.2.9 3.0.2.7 3.0.2.8 3.0.2.6 trunk 1.0.0 2.0.0 2.0.1 2.0.10 All 83 releases
yatra / app / Helpers / FormatHelper.php

FormatHelper.php in Yatra – Travel Booking & Tour Operator Software 3.0.8, at app/Helpers/FormatHelper.php

685 lines 27.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 declare(strict_types=1);
4
5 namespace Yatra\Helpers;
6
7 use Yatra\Services\SettingsService;
8
9 /**
10 * Format Helper
11 *
12 * Utility functions for formatting data.
13 * All methods are static for easy access throughout the application.
14 *
15 * Usage: FormatHelper::formatPrice(100, 'USD')
16 *
17 * @package Yatra\Helpers
18 */
19 class FormatHelper
20 {
21 // Currency symbols are now managed by CurrencyHelper
22
23 /**
24 * Country codes to names mapping
25 */
26 /**
27 * Country code => translated display name.
28 *
29 * Names are wrapped in __() at the definition so every consumer (booking
30 * form, admin, confirmation) shows localized names, and so all of them are
31 * extractable into the translation catalog. Cached per request.
32 */
33 private static ?array $countryNamesCache = null;
34
35 private static function countryNames(): array
36 {
37 if (self::$countryNamesCache !== null) {
38 return self::$countryNamesCache;
39 }
40
41 return self::$countryNamesCache = [
42
43 'AF' => __('Afghanistan', 'yatra'), 'AL' => __('Albania', 'yatra'), 'DZ' => __('Algeria', 'yatra'), 'AD' => __('Andorra', 'yatra'),
44 'AO' => __('Angola', 'yatra'), 'AG' => __('Antigua and Barbuda', 'yatra'), 'AR' => __('Argentina', 'yatra'), 'AM' => __('Armenia', 'yatra'),
45 'AU' => __('Australia', 'yatra'), 'AT' => __('Austria', 'yatra'), 'AZ' => __('Azerbaijan', 'yatra'), 'BS' => __('Bahamas', 'yatra'),
46 'BH' => __('Bahrain', 'yatra'), 'BD' => __('Bangladesh', 'yatra'), 'BB' => __('Barbados', 'yatra'), 'BY' => __('Belarus', 'yatra'),
47 'BE' => __('Belgium', 'yatra'), 'BZ' => __('Belize', 'yatra'), 'BJ' => __('Benin', 'yatra'), 'BT' => __('Bhutan', 'yatra'),
48 'BO' => __('Bolivia', 'yatra'), 'BA' => __('Bosnia and Herzegovina', 'yatra'), 'BW' => __('Botswana', 'yatra'), 'BR' => __('Brazil', 'yatra'),
49 'BN' => __('Brunei', 'yatra'), 'BG' => __('Bulgaria', 'yatra'), 'BF' => __('Burkina Faso', 'yatra'), 'BI' => __('Burundi', 'yatra'),
50 'KH' => __('Cambodia', 'yatra'), 'CM' => __('Cameroon', 'yatra'), 'CA' => __('Canada', 'yatra'), 'CV' => __('Cape Verde', 'yatra'),
51 'CF' => __('Central African Republic', 'yatra'), 'TD' => __('Chad', 'yatra'), 'CL' => __('Chile', 'yatra'), 'CN' => __('China', 'yatra'),
52 'CO' => __('Colombia', 'yatra'), 'KM' => __('Comoros', 'yatra'), 'CG' => __('Congo', 'yatra'), 'CD' => __('DR Congo', 'yatra'),
53 'CR' => __('Costa Rica', 'yatra'), 'CI' => __('Ivory Coast', 'yatra'), 'HR' => __('Croatia', 'yatra'), 'CU' => __('Cuba', 'yatra'),
54 'CY' => __('Cyprus', 'yatra'), 'CZ' => __('Czech Republic', 'yatra'), 'DK' => __('Denmark', 'yatra'), 'DJ' => __('Djibouti', 'yatra'),
55 'DM' => __('Dominica', 'yatra'), 'DO' => __('Dominican Republic', 'yatra'), 'EC' => __('Ecuador', 'yatra'), 'EG' => __('Egypt', 'yatra'),
56 'SV' => __('El Salvador', 'yatra'), 'GQ' => __('Equatorial Guinea', 'yatra'), 'ER' => __('Eritrea', 'yatra'), 'EE' => __('Estonia', 'yatra'),
57 'SZ' => __('Eswatini', 'yatra'), 'ET' => __('Ethiopia', 'yatra'), 'FJ' => __('Fiji', 'yatra'), 'FI' => __('Finland', 'yatra'),
58 'FR' => __('France', 'yatra'), 'GA' => __('Gabon', 'yatra'), 'GM' => __('Gambia', 'yatra'), 'GE' => __('Georgia', 'yatra'),
59 'DE' => __('Germany', 'yatra'), 'GH' => __('Ghana', 'yatra'), 'GR' => __('Greece', 'yatra'), 'GD' => __('Grenada', 'yatra'),
60 'GT' => __('Guatemala', 'yatra'), 'GN' => __('Guinea', 'yatra'), 'GW' => __('Guinea-Bissau', 'yatra'), 'GY' => __('Guyana', 'yatra'),
61 'HT' => __('Haiti', 'yatra'), 'HN' => __('Honduras', 'yatra'), 'HU' => __('Hungary', 'yatra'), 'IS' => __('Iceland', 'yatra'),
62 'IN' => __('India', 'yatra'), 'ID' => __('Indonesia', 'yatra'), 'IR' => __('Iran', 'yatra'), 'IQ' => __('Iraq', 'yatra'),
63 'IE' => __('Ireland', 'yatra'), 'IL' => __('Israel', 'yatra'), 'IT' => __('Italy', 'yatra'), 'JM' => __('Jamaica', 'yatra'),
64 'JP' => __('Japan', 'yatra'), 'JO' => __('Jordan', 'yatra'), 'KZ' => __('Kazakhstan', 'yatra'), 'KE' => __('Kenya', 'yatra'),
65 'KI' => __('Kiribati', 'yatra'), 'KP' => __('North Korea', 'yatra'), 'KR' => __('South Korea', 'yatra'), 'KW' => __('Kuwait', 'yatra'),
66 'KG' => __('Kyrgyzstan', 'yatra'), 'LA' => __('Laos', 'yatra'), 'LV' => __('Latvia', 'yatra'), 'LB' => __('Lebanon', 'yatra'),
67 'LS' => __('Lesotho', 'yatra'), 'LR' => __('Liberia', 'yatra'), 'LY' => __('Libya', 'yatra'), 'LI' => __('Liechtenstein', 'yatra'),
68 'LT' => __('Lithuania', 'yatra'), 'LU' => __('Luxembourg', 'yatra'), 'MG' => __('Madagascar', 'yatra'), 'MW' => __('Malawi', 'yatra'),
69 'MY' => __('Malaysia', 'yatra'), 'MV' => __('Maldives', 'yatra'), 'ML' => __('Mali', 'yatra'), 'MT' => __('Malta', 'yatra'),
70 'MH' => __('Marshall Islands', 'yatra'), 'MR' => __('Mauritania', 'yatra'), 'MU' => __('Mauritius', 'yatra'), 'MX' => __('Mexico', 'yatra'),
71 'FM' => __('Micronesia', 'yatra'), 'MD' => __('Moldova', 'yatra'), 'MC' => __('Monaco', 'yatra'), 'MN' => __('Mongolia', 'yatra'),
72 'ME' => __('Montenegro', 'yatra'), 'MA' => __('Morocco', 'yatra'), 'MZ' => __('Mozambique', 'yatra'), 'MM' => __('Myanmar', 'yatra'),
73 'NA' => __('Namibia', 'yatra'), 'NR' => __('Nauru', 'yatra'), 'NP' => __('Nepal', 'yatra'), 'NL' => __('Netherlands', 'yatra'),
74 'NZ' => __('New Zealand', 'yatra'), 'NI' => __('Nicaragua', 'yatra'), 'NE' => __('Niger', 'yatra'), 'NG' => __('Nigeria', 'yatra'),
75 'MK' => __('North Macedonia', 'yatra'), 'NO' => __('Norway', 'yatra'), 'OM' => __('Oman', 'yatra'), 'PK' => __('Pakistan', 'yatra'),
76 'PW' => __('Palau', 'yatra'), 'PS' => __('Palestine', 'yatra'), 'PA' => __('Panama', 'yatra'), 'PG' => __('Papua New Guinea', 'yatra'),
77 'PY' => __('Paraguay', 'yatra'), 'PE' => __('Peru', 'yatra'), 'PH' => __('Philippines', 'yatra'), 'PL' => __('Poland', 'yatra'),
78 'PT' => __('Portugal', 'yatra'), 'QA' => __('Qatar', 'yatra'), 'RO' => __('Romania', 'yatra'), 'RU' => __('Russia', 'yatra'),
79 'RW' => __('Rwanda', 'yatra'), 'KN' => __('Saint Kitts and Nevis', 'yatra'), 'LC' => __('Saint Lucia', 'yatra'),
80 'VC' => __('Saint Vincent and the Grenadines', 'yatra'), 'WS' => __('Samoa', 'yatra'), 'SM' => __('San Marino', 'yatra'),
81 'ST' => __('Sao Tome and Principe', 'yatra'), 'SA' => __('Saudi Arabia', 'yatra'), 'SN' => __('Senegal', 'yatra'),
82 'RS' => __('Serbia', 'yatra'), 'SC' => __('Seychelles', 'yatra'), 'SL' => __('Sierra Leone', 'yatra'), 'SG' => __('Singapore', 'yatra'),
83 'SK' => __('Slovakia', 'yatra'), 'SI' => __('Slovenia', 'yatra'), 'SB' => __('Solomon Islands', 'yatra'), 'SO' => __('Somalia', 'yatra'),
84 'ZA' => __('South Africa', 'yatra'), 'SS' => __('South Sudan', 'yatra'), 'ES' => __('Spain', 'yatra'), 'LK' => __('Sri Lanka', 'yatra'),
85 'SD' => __('Sudan', 'yatra'), 'SR' => __('Suriname', 'yatra'), 'SE' => __('Sweden', 'yatra'), 'CH' => __('Switzerland', 'yatra'),
86 'SY' => __('Syria', 'yatra'), 'TW' => __('Taiwan', 'yatra'), 'TJ' => __('Tajikistan', 'yatra'), 'TZ' => __('Tanzania', 'yatra'),
87 'TH' => __('Thailand', 'yatra'), 'TL' => __('Timor-Leste', 'yatra'), 'TG' => __('Togo', 'yatra'), 'TO' => __('Tonga', 'yatra'),
88 'TT' => __('Trinidad and Tobago', 'yatra'), 'TN' => __('Tunisia', 'yatra'), 'TR' => __('Turkey', 'yatra'), 'TM' => __('Turkmenistan', 'yatra'),
89 'TV' => __('Tuvalu', 'yatra'), 'UG' => __('Uganda', 'yatra'), 'UA' => __('Ukraine', 'yatra'), 'AE' => __('United Arab Emirates', 'yatra'),
90 'GB' => __('United Kingdom', 'yatra'), 'US' => __('United States', 'yatra'), 'UY' => __('Uruguay', 'yatra'), 'UZ' => __('Uzbekistan', 'yatra'),
91 'VU' => __('Vanuatu', 'yatra'), 'VA' => __('Vatican City', 'yatra'), 'VE' => __('Venezuela', 'yatra'), 'VN' => __('Vietnam', 'yatra'),
92 'YE' => __('Yemen', 'yatra'), 'ZM' => __('Zambia', 'yatra'), 'ZW' => __('Zimbabwe', 'yatra'),
93 // ISO-3166-1 territories and special regions added so customers
94 // from common tourism markets (Hong Kong, Puerto Rico, Greenland,
95 // Gibraltar, Faroe Islands, etc.) can select their location.
96 'HK' => __('Hong Kong', 'yatra'), 'MO' => __('Macao', 'yatra'), 'PR' => __('Puerto Rico', 'yatra'),
97 'GI' => __('Gibraltar', 'yatra'), 'GL' => __('Greenland', 'yatra'), 'FO' => __('Faroe Islands', 'yatra'),
98 'GG' => __('Guernsey', 'yatra'), 'IM' => __('Isle of Man', 'yatra'), 'JE' => __('Jersey', 'yatra'),
99 'AX' => __('Aland Islands', 'yatra'), 'SJ' => __('Svalbard and Jan Mayen', 'yatra'),
100 'BM' => __('Bermuda', 'yatra'), 'KY' => __('Cayman Islands', 'yatra'), 'AI' => __('Anguilla', 'yatra'),
101 'AW' => __('Aruba', 'yatra'), 'CW' => __('Curacao', 'yatra'), 'SX' => __('Sint Maarten', 'yatra'),
102 'BQ' => __('Bonaire, Sint Eustatius and Saba', 'yatra'),
103 'MS' => __('Montserrat', 'yatra'), 'VG' => __('British Virgin Islands', 'yatra'),
104 'TC' => __('Turks and Caicos Islands', 'yatra'),
105 'BL' => __('Saint Barthelemy', 'yatra'), 'MF' => __('Saint Martin (French)', 'yatra'),
106 'PM' => __('Saint Pierre and Miquelon', 'yatra'),
107 'SH' => __('Saint Helena, Ascension and Tristan da Cunha', 'yatra'),
108 'FK' => __('Falkland Islands', 'yatra'),
109 'GS' => __('South Georgia and the South Sandwich Islands', 'yatra'),
110 'PF' => __('French Polynesia', 'yatra'), 'NC' => __('New Caledonia', 'yatra'),
111 'WF' => __('Wallis and Futuna', 'yatra'), 'CK' => __('Cook Islands', 'yatra'),
112 'NU' => __('Niue', 'yatra'), 'TK' => __('Tokelau', 'yatra'),
113 'GP' => __('Guadeloupe', 'yatra'), 'MQ' => __('Martinique', 'yatra'),
114 'RE' => __('Reunion', 'yatra'), 'YT' => __('Mayotte', 'yatra'),
115 'AS' => __('American Samoa', 'yatra'), 'GU' => __('Guam', 'yatra'),
116 'MP' => __('Northern Mariana Islands', 'yatra'), 'VI' => __('U.S. Virgin Islands', 'yatra'),
117 'UM' => __('U.S. Minor Outlying Islands', 'yatra'),
118 'TF' => __('French Southern Territories', 'yatra'),
119 'IO' => __('British Indian Ocean Territory', 'yatra'), 'BV' => __('Bouvet Island', 'yatra'),
120 'HM' => __('Heard Island and McDonald Islands', 'yatra'),
121 'AQ' => __('Antarctica', 'yatra'), 'PN' => __('Pitcairn', 'yatra'),
122 'EH' => __('Western Sahara', 'yatra'), 'XK' => __('Kosovo', 'yatra'),
123 ];
124 }
125
126 /**
127 * Format price with currency
128 *
129 * @param float $amount Amount to format
130 * @param string $currency Currency code (default: USD)
131 * @param bool $showCode Show currency code alongside symbol
132 * @return string Formatted price
133 */
134 public static function formatPrice(float $amount, string $currency = 'USD', bool $showCode = false): string
135 {
136 if (function_exists('yatra_format_price')) {
137 $main = yatra_format_price($amount, $currency, false);
138 } else {
139 $main = CurrencyHelper::format($amount, $currency, false);
140 }
141
142 if ($showCode) {
143 return $main . ' ' . strtoupper($currency);
144 }
145
146 return $main;
147 }
148
149 /**
150 * Get currency symbol
151 *
152 * @param string $currency Currency code
153 * @return string Currency symbol
154 */
155 public static function getCurrencySymbol(string $currency): string
156 {
157 return CurrencyHelper::getSymbol($currency);
158 }
159
160 /**
161 * Format date for display
162 *
163 * @param string $date Date string
164 * @param string $format PHP date format (default: from WordPress settings)
165 * @return string Formatted date
166 */
167 public static function formatDate(string $date, string $format = ''): string
168 {
169 if (empty($date)) {
170 return '';
171 }
172
173 $format = $format ?: SettingsService::getString('date_format', (string) get_option('date_format', 'F j, Y'));
174
175 $timestamp = strtotime($date);
176 if ($timestamp === false) {
177 return $date;
178 }
179
180 return self::formatTimestampWithTz($timestamp, $format);
181 }
182
183 /**
184 * Format datetime for display
185 *
186 * @param string $datetime Datetime string
187 * @param string $format PHP date format
188 * @return string Formatted datetime
189 */
190 public static function formatDateTime(string $datetime, string $format = ''): string
191 {
192 if (empty($datetime)) {
193 return '';
194 }
195
196 $dateFormat = SettingsService::getString('date_format', (string) get_option('date_format', 'F j, Y'));
197 $timeFormat = SettingsService::getString('time_format', (string) get_option('time_format', 'g:i a'));
198 $format = $format ?: $dateFormat . ' ' . $timeFormat;
199
200 $timestamp = strtotime($datetime);
201 if ($timestamp === false) {
202 return $datetime;
203 }
204
205 return self::formatTimestampWithTz($timestamp, $format);
206 }
207
208 /**
209 * Format time for display
210 *
211 * @param string $time Time string (e.g., "14:30")
212 * @return string Formatted time (e.g., "2:30 PM")
213 */
214 public static function formatTime(string $time): string
215 {
216 return self::formatTimeForDisplay($time);
217 }
218
219 /**
220 * Format time for display using plugin settings (fallback to WordPress settings)
221 *
222 * @param string $time Time string (e.g., "14:30")
223 * @return string Formatted time (e.g., "2:30 PM")
224 */
225 public static function formatTimeForDisplay(string $time): string
226 {
227 if (empty($time)) {
228 return '';
229 }
230
231 $timestamp = strtotime($time);
232 if ($timestamp === false) {
233 return $time;
234 }
235
236 $format = SettingsService::getString('time_format', (string) get_option('time_format', 'g:i a'));
237 return self::formatTimestampWithTz($timestamp, $format);
238 }
239
240 /**
241 * Format a timestamp using Yatra timezone when set.
242 *
243 * @param int $timestamp Unix timestamp
244 * @param string $format PHP date format
245 * @return string
246 */
247 private static function formatTimestampWithTz(int $timestamp, string $format): string
248 {
249 $tz = trim((string) SettingsService::getString('timezone', ''));
250 if ($tz === '') {
251 // WP site timezone fallback
252 return function_exists('wp_date')
253 ? wp_date($format, $timestamp)
254 : date_i18n($format, $timestamp);
255 }
256
257 try {
258 $dtz = new \DateTimeZone($tz);
259 } catch (\Exception $e) {
260 return function_exists('wp_date')
261 ? wp_date($format, $timestamp)
262 : date_i18n($format, $timestamp);
263 }
264
265 if (function_exists('wp_date')) {
266 return wp_date($format, $timestamp, $dtz);
267 }
268
269 // Older WP fallback: shift timestamp into requested timezone via DateTime.
270 $d = new \DateTime('@' . $timestamp);
271 $d->setTimezone($dtz);
272 return $d->format($format);
273 }
274
275 /**
276 * Get relative time (e.g., "2 hours ago")
277 *
278 * @param string $datetime Datetime string
279 * @return string Relative time
280 */
281 public static function timeAgo(string $datetime): string
282 {
283 if (empty($datetime)) {
284 return '';
285 }
286
287 $timestamp = strtotime($datetime);
288 if ($timestamp === false) {
289 return $datetime;
290 }
291
292 return human_time_diff($timestamp, current_time('timestamp')) . ' ' . __('ago', 'yatra');
293 }
294
295 /**
296 * Get country name from code
297 *
298 * @param string $code Country code (e.g., "US")
299 * @return string Country name
300 */
301 public static function getCountryName(string $code): string
302 {
303 $code = strtoupper($code);
304 return self::countryNames()[$code] ?? $code;
305 }
306
307 /**
308 * Canonical country list — single source of truth used by every
309 * country / nationality dropdown in both Free and Pro plugins.
310 *
311 * Returns the full ISO-3166-1 alpha-2 set (sovereign states +
312 * dependent territories + commonly-accepted regions). Sorted
313 * alphabetically by name so the rendered dropdown is browseable
314 * without operators having to scan a code-ordered list.
315 *
316 * Filterable via `yatra_countries_list` for operators that want
317 * to:
318 * - Prepend "popular" entries (US, GB, IN, etc.) above an
319 * `---` separator for faster picking
320 * - Remove territories that don't apply to their market
321 * - Rename a region (e.g. business-language preferences)
322 *
323 * @return array<string, string> [ISO-3166 code => display name]
324 */
325 public static function getCountries(): array
326 {
327 $countries = self::countryNames();
328
329 // Stable alphabetical sort by display name. Operators expect
330 // "Argentina" before "Australia" before "Austria" — code-order
331 // (AR/AU/AT) is computer-friendly but not human-friendly.
332 asort($countries, SORT_STRING | SORT_FLAG_CASE);
333
334 if (\function_exists('apply_filters')) {
335 $filtered = apply_filters('yatra_countries_list', $countries);
336 if (\is_array($filtered) && $filtered !== []) {
337 return $filtered;
338 }
339 }
340 return $countries;
341 }
342
343 /**
344 * Format phone number for display
345 *
346 * @param string $phone Phone number
347 * @return string Formatted phone
348 */
349 public static function formatPhone(string $phone): string
350 {
351 // Remove non-numeric characters except + and spaces
352 $phone = preg_replace('/[^\d\+\s\-\(\)]/', '', $phone);
353 return trim($phone);
354 }
355
356 /**
357 * Format duration (days/nights)
358 *
359 * @param int $days Number of days
360 * @param int|null $nights Number of nights (optional)
361 * @return string Formatted duration
362 */
363 public static function formatDuration(int $days, ?int $nights = null): string
364 {
365 if ($nights !== null) {
366 return sprintf(
367 _n('%d Day', '%d Days', $days, 'yatra'),
368 $days
369 ) . ' / ' . sprintf(
370 _n('%d Night', '%d Nights', $nights, 'yatra'),
371 $nights
372 );
373 }
374
375 return sprintf(_n('%d Day', '%d Days', $days, 'yatra'), $days);
376 }
377
378 /**
379 * Format file size
380 *
381 * @param int $bytes File size in bytes
382 * @return string Formatted size
383 */
384 public static function formatFileSize(int $bytes): string
385 {
386 $units = ['B', 'KB', 'MB', 'GB', 'TB'];
387 $unitIndex = 0;
388
389 while ($bytes >= 1024 && $unitIndex < count($units) - 1) {
390 $bytes /= 1024;
391 $unitIndex++;
392 }
393
394 return round($bytes, 2) . ' ' . $units[$unitIndex];
395 }
396
397 /**
398 * Truncate text with ellipsis
399 *
400 * @param string $text Text to truncate
401 * @param int $length Maximum length
402 * @param string $suffix Suffix to add (default: ...)
403 * @return string Truncated text
404 */
405 public static function truncate(string $text, int $length = 100, string $suffix = '...'): string
406 {
407 if (mb_strlen($text) <= $length) {
408 return $text;
409 }
410
411 return mb_substr($text, 0, $length - mb_strlen($suffix)) . $suffix;
412 }
413
414 /**
415 * Generate excerpt from HTML content
416 *
417 * @param string $html HTML content
418 * @param int $length Maximum length
419 * @return string Plain text excerpt
420 */
421 public static function excerpt(string $html, int $length = 150): string
422 {
423 $text = wp_strip_all_tags($html);
424 $text = preg_replace('/\s+/', ' ', $text);
425 $text = trim($text);
426
427 return self::truncate($text, $length);
428 }
429
430 /**
431 * Format rating as stars HTML
432 *
433 * @param float $rating Rating (0-5)
434 * @param bool $showNumber Show rating number
435 * @return string HTML stars
436 */
437 public static function formatRatingStars(float $rating, bool $showNumber = false): string
438 {
439 $rating = max(0, min(5, $rating));
440 $fullStars = (int) floor($rating);
441 $hasHalfStar = ($rating - $fullStars) >= 0.5;
442
443 $html = '<span class="yatra-rating-stars">';
444
445 for ($i = 1; $i <= 5; $i++) {
446 if ($i <= $fullStars) {
447 $html .= '<span class="yatra-star filled">�
448 </span>';
449 } elseif ($i === $fullStars + 1 && $hasHalfStar) {
450 $html .= '<span class="yatra-star half">�
451 </span>';
452 } else {
453 $html .= '<span class="yatra-star">�
454 </span>';
455 }
456 }
457
458 $html .= '</span>';
459
460 if ($showNumber) {
461 $html .= '<span class="yatra-rating-number">' . number_format($rating, 1) . '</span>';
462 }
463
464 return $html;
465 }
466
467 /**
468 * Format booking status as badge
469 *
470 * @param string $status Booking status
471 * @return string HTML badge
472 */
473 public static function formatStatusBadge(string $status): string
474 {
475 $statusClasses = [
476 'pending' => 'yatra-badge yatra-badge-warning',
477 'confirmed' => 'yatra-badge yatra-badge-success',
478 'processing' => 'yatra-badge yatra-badge-info',
479 'completed' => 'yatra-badge yatra-badge-success',
480 'cancelled' => 'yatra-badge yatra-badge-danger',
481 'refunded' => 'yatra-badge yatra-badge-secondary',
482 'failed' => 'yatra-badge yatra-badge-danger',
483 'on_hold' => 'yatra-badge yatra-badge-warning',
484 ];
485
486 $class = $statusClasses[$status] ?? 'yatra-badge';
487 $label = ucfirst(str_replace('_', ' ', $status));
488
489 return '<span class="' . esc_attr($class) . '">' . esc_html($label) . '</span>';
490 }
491
492 /**
493 * Sanitize and format slug
494 *
495 * @param string $text Text to slugify
496 * @return string URL-safe slug
497 */
498 public static function slugify(string $text): string
499 {
500 return sanitize_title($text);
501 }
502
503 /**
504 * Convert array to HTML attributes string
505 *
506 * @param array $attributes Key-value pairs
507 * @return string HTML attributes
508 */
509 public static function arrayToAttributes(array $attributes): string
510 {
511 $html = [];
512
513 foreach ($attributes as $key => $value) {
514 if ($value === true) {
515 $html[] = esc_attr($key);
516 } elseif ($value !== false && $value !== null) {
517 $html[] = esc_attr($key) . '="' . esc_attr($value) . '"';
518 }
519 }
520
521 return implode(' ', $html);
522 }
523
524 /**
525 * Sanitize Quill editor HTML output
526 *
527 * This function sanitizes HTML content from the Quill rich text editor,
528 * allowing only safe HTML tags and attributes that match the Quill toolbar configuration.
529 *
530 * Allowed features based on Quill toolbar:
531 * - Headers: h1, h2, h3
532 * - Text formatting: bold, italic, underline, strike
533 * - Lists: ordered (ol, li), unordered (ul, li)
534 * - Alignment: text-align attribute on p tags
535 * - Links: a tags with href attribute
536 * - Paragraphs: p tags
537 *
538 * @param string $html Raw HTML from Quill editor
539 * @return string Sanitized HTML safe for database storage
540 */
541 public static function sanitizeQuillHtml(string $html): string
542 {
543 // Return empty string if input is empty or just whitespace
544 if (empty(trim($html)) || $html === '<p><br></p>') {
545 return '';
546 }
547
548 // Define allowed HTML tags and attributes based on Quill configuration
549 $allowed_tags = [
550 // Headers (from Quill header dropdown: 1, 2, 3)
551 'h1' => [],
552 'h2' => [],
553 'h3' => [],
554
555 // Paragraphs with alignment support
556 'p' => [
557 'style' => true, // For text-align
558 'class' => true, // Quill may add alignment classes
559 ],
560
561 // Text formatting (bold, italic, underline, strike)
562 'strong' => [],
563 'b' => [],
564 'em' => [],
565 'i' => [],
566 'u' => [],
567 's' => [],
568 'strike' => [],
569
570 // Lists (ordered and unordered)
571 'ol' => [],
572 'ul' => [],
573 'li' => [],
574
575 // Links
576 'a' => [
577 'href' => true,
578 'title' => true,
579 'target' => true,
580 'rel' => true,
581 ],
582
583 // Line breaks
584 'br' => [],
585 ];
586
587 // Use wp_kses to sanitize with allowed tags
588 $sanitized = wp_kses($html, $allowed_tags);
589
590 // Additional cleanup for alignment styles
591 // Only allow text-align in style attribute
592 $sanitized = preg_replace_callback(
593 '/style="([^"]*)"/i',
594 function ($matches) {
595 $styles = $matches[1];
596 // Extract only text-align property
597 if (preg_match('/text-align:\s*(left|center|right|justify)/i', $styles, $align)) {
598 return 'style="text-align: ' . esc_attr($align[1]) . '"';
599 }
600 return ''; // Remove style attribute if no valid text-align
601 },
602 $sanitized
603 );
604
605 // Ensure links have proper rel attribute for security
606 $sanitized = preg_replace_callback(
607 '/<a\s+([^>]*?)>/i',
608 function ($matches) {
609 $attrs = $matches[1];
610 // If target="_blank" exists, ensure rel="noopener noreferrer"
611 if (stripos($attrs, 'target="_blank"') !== false) {
612 if (stripos($attrs, 'rel=') === false) {
613 $attrs .= ' rel="noopener noreferrer"';
614 } elseif (stripos($attrs, 'noopener') === false || stripos($attrs, 'noreferrer') === false) {
615 $attrs = preg_replace(
616 '/rel="([^"]*)"/i',
617 'rel="$1 noopener noreferrer"',
618 $attrs
619 );
620 }
621 }
622 return '<a ' . $attrs . '>';
623 },
624 $sanitized
625 );
626
627 // Remove empty paragraphs and normalize whitespace
628 $sanitized = preg_replace('/<p[^>]*>(\s|&nbsp;|<br\s*\/?>)*<\/p>/i', '', $sanitized);
629
630 // Trim whitespace
631 $sanitized = trim($sanitized);
632
633 return $sanitized;
634 }
635
636 /**
637 * Prepare Quill HTML for display (output escaping)
638 *
639 * Use this when outputting sanitized Quill content to the frontend.
640 * This assumes the content was already sanitized with sanitizeQuillHtml() before storage.
641 *
642 * @param string $html Sanitized HTML from database
643 * @return string HTML safe for display
644 */
645 public static function displayQuillHtml(string $html): string
646 {
647 if (empty($html)) {
648 return '';
649 }
650
651 // Apply WordPress content filters (auto-paragraphs, shortcodes, etc.)
652 // But skip wpautop since Quill already handles paragraphs
653 remove_filter('the_content', 'wpautop');
654 $content = apply_filters('the_content', $html);
655 add_filter('the_content', 'wpautop');
656
657 return $content;
658 }
659
660 /**
661 * Strip all HTML tags from Quill content (for excerpts, meta descriptions, etc.)
662 *
663 * @param string $html Quill HTML content
664 * @return string Plain text
665 */
666 public static function quillToPlainText(string $html): string
667 {
668 if (empty($html)) {
669 return '';
670 }
671
672 // Remove all HTML tags
673 $text = wp_strip_all_tags($html);
674
675 // Normalize whitespace
676 $text = preg_replace('/\s+/', ' ', $text);
677
678 // Trim
679 $text = trim($text);
680
681 return $text;
682 }
683 }
684
685