PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.3
Yatra – Travel Booking & Tour Operator Software v3.0.3
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 2.0.11 All 82 releases
yatra / app / Helpers / FormatHelper.php

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

612 lines 20.8 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 private const COUNTRY_NAMES = [
27 'AF' => 'Afghanistan', 'AL' => 'Albania', 'DZ' => 'Algeria', 'AD' => 'Andorra',
28 'AO' => 'Angola', 'AG' => 'Antigua and Barbuda', 'AR' => 'Argentina', 'AM' => 'Armenia',
29 'AU' => 'Australia', 'AT' => 'Austria', 'AZ' => 'Azerbaijan', 'BS' => 'Bahamas',
30 'BH' => 'Bahrain', 'BD' => 'Bangladesh', 'BB' => 'Barbados', 'BY' => 'Belarus',
31 'BE' => 'Belgium', 'BZ' => 'Belize', 'BJ' => 'Benin', 'BT' => 'Bhutan',
32 'BO' => 'Bolivia', 'BA' => 'Bosnia and Herzegovina', 'BW' => 'Botswana', 'BR' => 'Brazil',
33 'BN' => 'Brunei', 'BG' => 'Bulgaria', 'BF' => 'Burkina Faso', 'BI' => 'Burundi',
34 'KH' => 'Cambodia', 'CM' => 'Cameroon', 'CA' => 'Canada', 'CV' => 'Cape Verde',
35 'CF' => 'Central African Republic', 'TD' => 'Chad', 'CL' => 'Chile', 'CN' => 'China',
36 'CO' => 'Colombia', 'KM' => 'Comoros', 'CG' => 'Congo', 'CD' => 'DR Congo',
37 'CR' => 'Costa Rica', 'CI' => 'Ivory Coast', 'HR' => 'Croatia', 'CU' => 'Cuba',
38 'CY' => 'Cyprus', 'CZ' => 'Czech Republic', 'DK' => 'Denmark', 'DJ' => 'Djibouti',
39 'DM' => 'Dominica', 'DO' => 'Dominican Republic', 'EC' => 'Ecuador', 'EG' => 'Egypt',
40 'SV' => 'El Salvador', 'GQ' => 'Equatorial Guinea', 'ER' => 'Eritrea', 'EE' => 'Estonia',
41 'SZ' => 'Eswatini', 'ET' => 'Ethiopia', 'FJ' => 'Fiji', 'FI' => 'Finland',
42 'FR' => 'France', 'GA' => 'Gabon', 'GM' => 'Gambia', 'GE' => 'Georgia',
43 'DE' => 'Germany', 'GH' => 'Ghana', 'GR' => 'Greece', 'GD' => 'Grenada',
44 'GT' => 'Guatemala', 'GN' => 'Guinea', 'GW' => 'Guinea-Bissau', 'GY' => 'Guyana',
45 'HT' => 'Haiti', 'HN' => 'Honduras', 'HU' => 'Hungary', 'IS' => 'Iceland',
46 'IN' => 'India', 'ID' => 'Indonesia', 'IR' => 'Iran', 'IQ' => 'Iraq',
47 'IE' => 'Ireland', 'IL' => 'Israel', 'IT' => 'Italy', 'JM' => 'Jamaica',
48 'JP' => 'Japan', 'JO' => 'Jordan', 'KZ' => 'Kazakhstan', 'KE' => 'Kenya',
49 'KI' => 'Kiribati', 'KP' => 'North Korea', 'KR' => 'South Korea', 'KW' => 'Kuwait',
50 'KG' => 'Kyrgyzstan', 'LA' => 'Laos', 'LV' => 'Latvia', 'LB' => 'Lebanon',
51 'LS' => 'Lesotho', 'LR' => 'Liberia', 'LY' => 'Libya', 'LI' => 'Liechtenstein',
52 'LT' => 'Lithuania', 'LU' => 'Luxembourg', 'MG' => 'Madagascar', 'MW' => 'Malawi',
53 'MY' => 'Malaysia', 'MV' => 'Maldives', 'ML' => 'Mali', 'MT' => 'Malta',
54 'MH' => 'Marshall Islands', 'MR' => 'Mauritania', 'MU' => 'Mauritius', 'MX' => 'Mexico',
55 'FM' => 'Micronesia', 'MD' => 'Moldova', 'MC' => 'Monaco', 'MN' => 'Mongolia',
56 'ME' => 'Montenegro', 'MA' => 'Morocco', 'MZ' => 'Mozambique', 'MM' => 'Myanmar',
57 'NA' => 'Namibia', 'NR' => 'Nauru', 'NP' => 'Nepal', 'NL' => 'Netherlands',
58 'NZ' => 'New Zealand', 'NI' => 'Nicaragua', 'NE' => 'Niger', 'NG' => 'Nigeria',
59 'MK' => 'North Macedonia', 'NO' => 'Norway', 'OM' => 'Oman', 'PK' => 'Pakistan',
60 'PW' => 'Palau', 'PS' => 'Palestine', 'PA' => 'Panama', 'PG' => 'Papua New Guinea',
61 'PY' => 'Paraguay', 'PE' => 'Peru', 'PH' => 'Philippines', 'PL' => 'Poland',
62 'PT' => 'Portugal', 'QA' => 'Qatar', 'RO' => 'Romania', 'RU' => 'Russia',
63 'RW' => 'Rwanda', 'KN' => 'Saint Kitts and Nevis', 'LC' => 'Saint Lucia',
64 'VC' => 'Saint Vincent and the Grenadines', 'WS' => 'Samoa', 'SM' => 'San Marino',
65 'ST' => 'Sao Tome and Principe', 'SA' => 'Saudi Arabia', 'SN' => 'Senegal',
66 'RS' => 'Serbia', 'SC' => 'Seychelles', 'SL' => 'Sierra Leone', 'SG' => 'Singapore',
67 'SK' => 'Slovakia', 'SI' => 'Slovenia', 'SB' => 'Solomon Islands', 'SO' => 'Somalia',
68 'ZA' => 'South Africa', 'SS' => 'South Sudan', 'ES' => 'Spain', 'LK' => 'Sri Lanka',
69 'SD' => 'Sudan', 'SR' => 'Suriname', 'SE' => 'Sweden', 'CH' => 'Switzerland',
70 'SY' => 'Syria', 'TW' => 'Taiwan', 'TJ' => 'Tajikistan', 'TZ' => 'Tanzania',
71 'TH' => 'Thailand', 'TL' => 'Timor-Leste', 'TG' => 'Togo', 'TO' => 'Tonga',
72 'TT' => 'Trinidad and Tobago', 'TN' => 'Tunisia', 'TR' => 'Turkey', 'TM' => 'Turkmenistan',
73 'TV' => 'Tuvalu', 'UG' => 'Uganda', 'UA' => 'Ukraine', 'AE' => 'United Arab Emirates',
74 'GB' => 'United Kingdom', 'US' => 'United States', 'UY' => 'Uruguay', 'UZ' => 'Uzbekistan',
75 'VU' => 'Vanuatu', 'VA' => 'Vatican City', 'VE' => 'Venezuela', 'VN' => 'Vietnam',
76 'YE' => 'Yemen', 'ZM' => 'Zambia', 'ZW' => 'Zimbabwe',
77 ];
78
79 /**
80 * Format price with currency
81 *
82 * @param float $amount Amount to format
83 * @param string $currency Currency code (default: USD)
84 * @param bool $showCode Show currency code alongside symbol
85 * @return string Formatted price
86 */
87 public static function formatPrice(float $amount, string $currency = 'USD', bool $showCode = false): string
88 {
89 if (function_exists('yatra_format_price')) {
90 $main = yatra_format_price($amount, $currency, false);
91 } else {
92 $main = CurrencyHelper::format($amount, $currency, false);
93 }
94
95 if ($showCode) {
96 return $main . ' ' . strtoupper($currency);
97 }
98
99 return $main;
100 }
101
102 /**
103 * Get currency symbol
104 *
105 * @param string $currency Currency code
106 * @return string Currency symbol
107 */
108 public static function getCurrencySymbol(string $currency): string
109 {
110 return CurrencyHelper::getSymbol($currency);
111 }
112
113 /**
114 * Format date for display
115 *
116 * @param string $date Date string
117 * @param string $format PHP date format (default: from WordPress settings)
118 * @return string Formatted date
119 */
120 public static function formatDate(string $date, string $format = ''): string
121 {
122 if (empty($date)) {
123 return '';
124 }
125
126 $format = $format ?: SettingsService::getString('date_format', (string) get_option('date_format', 'F j, Y'));
127
128 $timestamp = strtotime($date);
129 if ($timestamp === false) {
130 return $date;
131 }
132
133 return self::formatTimestampWithTz($timestamp, $format);
134 }
135
136 /**
137 * Format datetime for display
138 *
139 * @param string $datetime Datetime string
140 * @param string $format PHP date format
141 * @return string Formatted datetime
142 */
143 public static function formatDateTime(string $datetime, string $format = ''): string
144 {
145 if (empty($datetime)) {
146 return '';
147 }
148
149 $dateFormat = SettingsService::getString('date_format', (string) get_option('date_format', 'F j, Y'));
150 $timeFormat = SettingsService::getString('time_format', (string) get_option('time_format', 'g:i a'));
151 $format = $format ?: $dateFormat . ' ' . $timeFormat;
152
153 $timestamp = strtotime($datetime);
154 if ($timestamp === false) {
155 return $datetime;
156 }
157
158 return self::formatTimestampWithTz($timestamp, $format);
159 }
160
161 /**
162 * Format time for display
163 *
164 * @param string $time Time string (e.g., "14:30")
165 * @return string Formatted time (e.g., "2:30 PM")
166 */
167 public static function formatTime(string $time): string
168 {
169 return self::formatTimeForDisplay($time);
170 }
171
172 /**
173 * Format time for display using plugin settings (fallback to WordPress settings)
174 *
175 * @param string $time Time string (e.g., "14:30")
176 * @return string Formatted time (e.g., "2:30 PM")
177 */
178 public static function formatTimeForDisplay(string $time): string
179 {
180 if (empty($time)) {
181 return '';
182 }
183
184 $timestamp = strtotime($time);
185 if ($timestamp === false) {
186 return $time;
187 }
188
189 $format = SettingsService::getString('time_format', (string) get_option('time_format', 'g:i a'));
190 return self::formatTimestampWithTz($timestamp, $format);
191 }
192
193 /**
194 * Format a timestamp using Yatra timezone when set.
195 *
196 * @param int $timestamp Unix timestamp
197 * @param string $format PHP date format
198 * @return string
199 */
200 private static function formatTimestampWithTz(int $timestamp, string $format): string
201 {
202 $tz = trim((string) SettingsService::getString('timezone', ''));
203 if ($tz === '') {
204 // WP site timezone fallback
205 return function_exists('wp_date')
206 ? wp_date($format, $timestamp)
207 : date_i18n($format, $timestamp);
208 }
209
210 try {
211 $dtz = new \DateTimeZone($tz);
212 } catch (\Exception $e) {
213 return function_exists('wp_date')
214 ? wp_date($format, $timestamp)
215 : date_i18n($format, $timestamp);
216 }
217
218 if (function_exists('wp_date')) {
219 return wp_date($format, $timestamp, $dtz);
220 }
221
222 // Older WP fallback: shift timestamp into requested timezone via DateTime.
223 $d = new \DateTime('@' . $timestamp);
224 $d->setTimezone($dtz);
225 return $d->format($format);
226 }
227
228 /**
229 * Get relative time (e.g., "2 hours ago")
230 *
231 * @param string $datetime Datetime string
232 * @return string Relative time
233 */
234 public static function timeAgo(string $datetime): string
235 {
236 if (empty($datetime)) {
237 return '';
238 }
239
240 $timestamp = strtotime($datetime);
241 if ($timestamp === false) {
242 return $datetime;
243 }
244
245 return human_time_diff($timestamp, current_time('timestamp')) . ' ' . __('ago', 'yatra');
246 }
247
248 /**
249 * Get country name from code
250 *
251 * @param string $code Country code (e.g., "US")
252 * @return string Country name
253 */
254 public static function getCountryName(string $code): string
255 {
256 $code = strtoupper($code);
257 return self::COUNTRY_NAMES[$code] ?? $code;
258 }
259
260 /**
261 * Get all countries as options array
262 *
263 * @return array [code => name]
264 */
265 public static function getCountries(): array
266 {
267 return self::COUNTRY_NAMES;
268 }
269
270 /**
271 * Format phone number for display
272 *
273 * @param string $phone Phone number
274 * @return string Formatted phone
275 */
276 public static function formatPhone(string $phone): string
277 {
278 // Remove non-numeric characters except + and spaces
279 $phone = preg_replace('/[^\d\+\s\-\(\)]/', '', $phone);
280 return trim($phone);
281 }
282
283 /**
284 * Format duration (days/nights)
285 *
286 * @param int $days Number of days
287 * @param int|null $nights Number of nights (optional)
288 * @return string Formatted duration
289 */
290 public static function formatDuration(int $days, ?int $nights = null): string
291 {
292 if ($nights !== null) {
293 return sprintf(
294 _n('%d Day', '%d Days', $days, 'yatra'),
295 $days
296 ) . ' / ' . sprintf(
297 _n('%d Night', '%d Nights', $nights, 'yatra'),
298 $nights
299 );
300 }
301
302 return sprintf(_n('%d Day', '%d Days', $days, 'yatra'), $days);
303 }
304
305 /**
306 * Format file size
307 *
308 * @param int $bytes File size in bytes
309 * @return string Formatted size
310 */
311 public static function formatFileSize(int $bytes): string
312 {
313 $units = ['B', 'KB', 'MB', 'GB', 'TB'];
314 $unitIndex = 0;
315
316 while ($bytes >= 1024 && $unitIndex < count($units) - 1) {
317 $bytes /= 1024;
318 $unitIndex++;
319 }
320
321 return round($bytes, 2) . ' ' . $units[$unitIndex];
322 }
323
324 /**
325 * Truncate text with ellipsis
326 *
327 * @param string $text Text to truncate
328 * @param int $length Maximum length
329 * @param string $suffix Suffix to add (default: ...)
330 * @return string Truncated text
331 */
332 public static function truncate(string $text, int $length = 100, string $suffix = '...'): string
333 {
334 if (mb_strlen($text) <= $length) {
335 return $text;
336 }
337
338 return mb_substr($text, 0, $length - mb_strlen($suffix)) . $suffix;
339 }
340
341 /**
342 * Generate excerpt from HTML content
343 *
344 * @param string $html HTML content
345 * @param int $length Maximum length
346 * @return string Plain text excerpt
347 */
348 public static function excerpt(string $html, int $length = 150): string
349 {
350 $text = wp_strip_all_tags($html);
351 $text = preg_replace('/\s+/', ' ', $text);
352 $text = trim($text);
353
354 return self::truncate($text, $length);
355 }
356
357 /**
358 * Format rating as stars HTML
359 *
360 * @param float $rating Rating (0-5)
361 * @param bool $showNumber Show rating number
362 * @return string HTML stars
363 */
364 public static function formatRatingStars(float $rating, bool $showNumber = false): string
365 {
366 $rating = max(0, min(5, $rating));
367 $fullStars = (int) floor($rating);
368 $hasHalfStar = ($rating - $fullStars) >= 0.5;
369
370 $html = '<span class="yatra-rating-stars">';
371
372 for ($i = 1; $i <= 5; $i++) {
373 if ($i <= $fullStars) {
374 $html .= '<span class="yatra-star filled">�
375 </span>';
376 } elseif ($i === $fullStars + 1 && $hasHalfStar) {
377 $html .= '<span class="yatra-star half">�
378 </span>';
379 } else {
380 $html .= '<span class="yatra-star">�
381 </span>';
382 }
383 }
384
385 $html .= '</span>';
386
387 if ($showNumber) {
388 $html .= '<span class="yatra-rating-number">' . number_format($rating, 1) . '</span>';
389 }
390
391 return $html;
392 }
393
394 /**
395 * Format booking status as badge
396 *
397 * @param string $status Booking status
398 * @return string HTML badge
399 */
400 public static function formatStatusBadge(string $status): string
401 {
402 $statusClasses = [
403 'pending' => 'yatra-badge yatra-badge-warning',
404 'confirmed' => 'yatra-badge yatra-badge-success',
405 'processing' => 'yatra-badge yatra-badge-info',
406 'completed' => 'yatra-badge yatra-badge-success',
407 'cancelled' => 'yatra-badge yatra-badge-danger',
408 'refunded' => 'yatra-badge yatra-badge-secondary',
409 'failed' => 'yatra-badge yatra-badge-danger',
410 'on_hold' => 'yatra-badge yatra-badge-warning',
411 ];
412
413 $class = $statusClasses[$status] ?? 'yatra-badge';
414 $label = ucfirst(str_replace('_', ' ', $status));
415
416 return '<span class="' . esc_attr($class) . '">' . esc_html($label) . '</span>';
417 }
418
419 /**
420 * Sanitize and format slug
421 *
422 * @param string $text Text to slugify
423 * @return string URL-safe slug
424 */
425 public static function slugify(string $text): string
426 {
427 return sanitize_title($text);
428 }
429
430 /**
431 * Convert array to HTML attributes string
432 *
433 * @param array $attributes Key-value pairs
434 * @return string HTML attributes
435 */
436 public static function arrayToAttributes(array $attributes): string
437 {
438 $html = [];
439
440 foreach ($attributes as $key => $value) {
441 if ($value === true) {
442 $html[] = esc_attr($key);
443 } elseif ($value !== false && $value !== null) {
444 $html[] = esc_attr($key) . '="' . esc_attr($value) . '"';
445 }
446 }
447
448 return implode(' ', $html);
449 }
450
451 /**
452 * Sanitize Quill editor HTML output
453 *
454 * This function sanitizes HTML content from the Quill rich text editor,
455 * allowing only safe HTML tags and attributes that match the Quill toolbar configuration.
456 *
457 * Allowed features based on Quill toolbar:
458 * - Headers: h1, h2, h3
459 * - Text formatting: bold, italic, underline, strike
460 * - Lists: ordered (ol, li), unordered (ul, li)
461 * - Alignment: text-align attribute on p tags
462 * - Links: a tags with href attribute
463 * - Paragraphs: p tags
464 *
465 * @param string $html Raw HTML from Quill editor
466 * @return string Sanitized HTML safe for database storage
467 */
468 public static function sanitizeQuillHtml(string $html): string
469 {
470 // Return empty string if input is empty or just whitespace
471 if (empty(trim($html)) || $html === '<p><br></p>') {
472 return '';
473 }
474
475 // Define allowed HTML tags and attributes based on Quill configuration
476 $allowed_tags = [
477 // Headers (from Quill header dropdown: 1, 2, 3)
478 'h1' => [],
479 'h2' => [],
480 'h3' => [],
481
482 // Paragraphs with alignment support
483 'p' => [
484 'style' => true, // For text-align
485 'class' => true, // Quill may add alignment classes
486 ],
487
488 // Text formatting (bold, italic, underline, strike)
489 'strong' => [],
490 'b' => [],
491 'em' => [],
492 'i' => [],
493 'u' => [],
494 's' => [],
495 'strike' => [],
496
497 // Lists (ordered and unordered)
498 'ol' => [],
499 'ul' => [],
500 'li' => [],
501
502 // Links
503 'a' => [
504 'href' => true,
505 'title' => true,
506 'target' => true,
507 'rel' => true,
508 ],
509
510 // Line breaks
511 'br' => [],
512 ];
513
514 // Use wp_kses to sanitize with allowed tags
515 $sanitized = wp_kses($html, $allowed_tags);
516
517 // Additional cleanup for alignment styles
518 // Only allow text-align in style attribute
519 $sanitized = preg_replace_callback(
520 '/style="([^"]*)"/i',
521 function ($matches) {
522 $styles = $matches[1];
523 // Extract only text-align property
524 if (preg_match('/text-align:\s*(left|center|right|justify)/i', $styles, $align)) {
525 return 'style="text-align: ' . esc_attr($align[1]) . '"';
526 }
527 return ''; // Remove style attribute if no valid text-align
528 },
529 $sanitized
530 );
531
532 // Ensure links have proper rel attribute for security
533 $sanitized = preg_replace_callback(
534 '/<a\s+([^>]*?)>/i',
535 function ($matches) {
536 $attrs = $matches[1];
537 // If target="_blank" exists, ensure rel="noopener noreferrer"
538 if (stripos($attrs, 'target="_blank"') !== false) {
539 if (stripos($attrs, 'rel=') === false) {
540 $attrs .= ' rel="noopener noreferrer"';
541 } elseif (stripos($attrs, 'noopener') === false || stripos($attrs, 'noreferrer') === false) {
542 $attrs = preg_replace(
543 '/rel="([^"]*)"/i',
544 'rel="$1 noopener noreferrer"',
545 $attrs
546 );
547 }
548 }
549 return '<a ' . $attrs . '>';
550 },
551 $sanitized
552 );
553
554 // Remove empty paragraphs and normalize whitespace
555 $sanitized = preg_replace('/<p[^>]*>(\s|&nbsp;|<br\s*\/?>)*<\/p>/i', '', $sanitized);
556
557 // Trim whitespace
558 $sanitized = trim($sanitized);
559
560 return $sanitized;
561 }
562
563 /**
564 * Prepare Quill HTML for display (output escaping)
565 *
566 * Use this when outputting sanitized Quill content to the frontend.
567 * This assumes the content was already sanitized with sanitizeQuillHtml() before storage.
568 *
569 * @param string $html Sanitized HTML from database
570 * @return string HTML safe for display
571 */
572 public static function displayQuillHtml(string $html): string
573 {
574 if (empty($html)) {
575 return '';
576 }
577
578 // Apply WordPress content filters (auto-paragraphs, shortcodes, etc.)
579 // But skip wpautop since Quill already handles paragraphs
580 remove_filter('the_content', 'wpautop');
581 $content = apply_filters('the_content', $html);
582 add_filter('the_content', 'wpautop');
583
584 return $content;
585 }
586
587 /**
588 * Strip all HTML tags from Quill content (for excerpts, meta descriptions, etc.)
589 *
590 * @param string $html Quill HTML content
591 * @return string Plain text
592 */
593 public static function quillToPlainText(string $html): string
594 {
595 if (empty($html)) {
596 return '';
597 }
598
599 // Remove all HTML tags
600 $text = wp_strip_all_tags($html);
601
602 // Normalize whitespace
603 $text = preg_replace('/\s+/', ' ', $text);
604
605 // Trim
606 $text = trim($text);
607
608 return $text;
609 }
610 }
611
612