| @@ -13,95 +13,294 @@ | ||
| 13 | 13 | |
| 14 | 14 | /** |
| 15 | 15 | * Parses the email content for special elements. |
| 16 | 16 | * |
| 17 | - * @since 1.5.5 | |
| 17 | + * @since 1.15.2 (J) - 1.5.5 (WP) | |
| 18 | 18 | */ |
| 19 | 19 | final class VBOMailParser |
| 20 | 20 | { |
| 21 | - /** | |
| 22 | - * @var string the default mail wrapper regex pattern (HTML tag in visual editor) | |
| 23 | - */ | |
| 24 | - private static $wrapper_pattern = "/<hr class=\"vbo-editor-hl-mailwrapper\"\s*\/?>/"; | |
| 21 | + /** | |
| 22 | + * @var string the default mail wrapper regex pattern (HTML tag in visual editor) | |
| 23 | + */ | |
| 24 | + private static $wrapper_pattern = "/<hr class=\"vbo-editor-hl-mailwrapper\"\s*\/?>/"; | |
| 25 | 25 | |
| 26 | - /** | |
| 27 | - * Checks for any content wrapper HTML tags applied through | |
| 28 | - * the Visual Editor during the composing of an email message. | |
| 29 | - * | |
| 30 | - * @param string $content the full email content. | |
| 31 | - * | |
| 32 | - * @return string | |
| 33 | - */ | |
| 34 | - public static function checkWrapperSymbols($content) | |
| 35 | - { | |
| 36 | - $chunks = preg_split(static::$wrapper_pattern, (string)$content); | |
| 26 | + /** | |
| 27 | + * Applies a common method for replacing the special tags within a content. | |
| 28 | + * This will NOT include the conditional text rule tags. | |
| 29 | + * | |
| 30 | + * @param string $content The content to parse. | |
| 31 | + * @param array $booking Booking record. | |
| 32 | + * @param array $booking_rooms List of rooms booked. | |
| 33 | + * @param array $customer Optional customer record. | |
| 34 | + * | |
| 35 | + * @return string | |
| 36 | + * | |
| 37 | + * @since 1.17.6 (J) - 1.7.6 (WP) | |
| 38 | + */ | |
| 39 | + public static function replaceSpecialTags(string $content, array $booking, array $booking_rooms, array $customer = []) | |
| 40 | + { | |
| 41 | + if (!$booking || empty($booking['id'])) { | |
| 42 | + return $content; | |
| 43 | + } | |
| 37 | 44 | |
| 38 | - if (count($chunks) < 2) { | |
| 39 | - // no content wrapper used in email message | |
| 40 | - return $content; | |
| 41 | - } | |
| 45 | + if (!$booking_rooms) { | |
| 46 | + $booking_rooms = VikBooking::loadOrdersRoomsData($booking['id']); | |
| 47 | + } | |
| 42 | 48 | |
| 43 | - $layout_opening = self::getWrapperLayout(true); | |
| 44 | - $layout_closing = self::getWrapperLayout(false); | |
| 49 | + if (!$customer) { | |
| 50 | + $customer = VikBooking::getCPinInstance()->getCustomerFromBooking($booking['id']); | |
| 51 | + } | |
| 45 | 52 | |
| 46 | - $final_content = ''; | |
| 47 | - foreach ($chunks as $piece => $part) { | |
| 48 | - $is_even = ($piece === 0 || (($piece % 2) === 0)); | |
| 49 | - if (strlen(trim($part)) && !$is_even) { | |
| 50 | - $final_content .= $layout_opening . $part . $layout_closing; | |
| 51 | - } else { | |
| 52 | - $final_content .= $part; | |
| 53 | - } | |
| 54 | - } | |
| 53 | + // build and set customer name | |
| 54 | + $booking['customer_name'] = implode(' ', array_filter([($customer['first_name'] ?? ''), ($customer['last_name'] ?? '')])); | |
| 55 | 55 | |
| 56 | - return $final_content; | |
| 57 | - } | |
| 56 | + $vbo_df = VikBooking::getDateFormat(); | |
| 57 | + $datesep = VikBooking::getDateSeparator(); | |
| 58 | + $df = $vbo_df == "%d/%m/%Y" ? 'd/m/Y' : ($vbo_df == "%m/%d/%Y" ? 'm/d/Y' : 'Y-m-d'); | |
| 58 | 59 | |
| 59 | - /** | |
| 60 | - * Returns the current mail wrapper layout HTML code. | |
| 61 | - * | |
| 62 | - * @param bool $opening whether to get the opening or closing HTML. | |
| 63 | - * | |
| 64 | - * @return string | |
| 65 | - */ | |
| 66 | - public static function getWrapperLayout($opening = true) | |
| 67 | - { | |
| 68 | - // configuration field name | |
| 69 | - $opt_name = 'mail_wrapper_layout_' . ($opening ? 'opening' : 'closing'); | |
| 60 | + $content = str_replace('{customer_name}', $booking['customer_name'], $content); | |
| 61 | + $content = str_replace('{booking_id}', $booking['id'], $content); | |
| 62 | + $content = str_replace('{checkin_date}', date(str_replace("/", $datesep, $df), $booking['checkin']), $content); | |
| 63 | + $content = str_replace('{checkout_date}', date(str_replace("/", $datesep, $df), $booking['checkout']), $content); | |
| 64 | + $content = str_replace('{num_nights}', $booking['days'], $content); | |
| 70 | 65 | |
| 71 | - // access the configuration object | |
| 72 | - $config = VBOFactory::getConfig(); | |
| 66 | + $rooms_booked = []; | |
| 67 | + $tot_adults = 0; | |
| 68 | + $tot_children = 0; | |
| 69 | + $tot_guests = 0; | |
| 70 | + foreach ($booking_rooms as $broom) { | |
| 71 | + if (isset($rooms_booked[$broom['room_name']])) { | |
| 72 | + $rooms_booked[$broom['room_name']] += 1; | |
| 73 | + } else { | |
| 74 | + $rooms_booked[$broom['room_name']] = 1; | |
| 75 | + } | |
| 76 | + $tot_adults += (int) $broom['adults']; | |
| 77 | + $tot_children += (int) $broom['children']; | |
| 78 | + $tot_guests += ((int) $broom['adults'] + (int) $broom['children']); | |
| 79 | + } | |
| 80 | + $content = str_replace('{tot_adults}', $tot_adults, $content); | |
| 81 | + $content = str_replace('{tot_children}', $tot_children, $content); | |
| 82 | + $content = str_replace('{tot_guests}', $tot_guests, $content); | |
| 73 | 83 | |
| 74 | - // get the current configuration value | |
| 75 | - $layout = $config->get($opt_name); | |
| 84 | + $rooms_booked_quant = []; | |
| 85 | + foreach ($rooms_booked as $rname => $quant) { | |
| 86 | + $rooms_booked_quant[] = ($quant > 1 ? $quant.' ' : '').$rname; | |
| 87 | + } | |
| 88 | + $content = str_replace('{rooms_booked}', implode(', ', $rooms_booked_quant), $content); | |
| 76 | 89 | |
| 77 | - if (!$layout) { | |
| 78 | - // set and get default layout | |
| 79 | - $layout = self::getWrapperDefaultLayout($opening); | |
| 80 | - $config->set($opt_name, $layout); | |
| 81 | - } | |
| 90 | + $content = str_replace('{currency}', VikBooking::getCurrencySymb(), $content); | |
| 91 | + if (isset($booking['total'])) { | |
| 92 | + $content = str_replace('{total}', VikBooking::numberFormat($booking['total']), $content); | |
| 93 | + } | |
| 94 | + if (isset($booking['totpaid'])) { | |
| 95 | + $content = str_replace('{total_paid}', VikBooking::numberFormat($booking['totpaid']), $content); | |
| 96 | + } | |
| 97 | + if (isset($booking['total']) && isset($booking['totpaid'])) { | |
| 98 | + // calculate the outstanding balance | |
| 99 | + $remaining_bal = $booking['total'] - $booking['totpaid']; | |
| 100 | + $damage_deposit_payment = VBORoomHelper::getInstance()->getDamageDepositSplitPayment($booking, $booking_rooms); | |
| 101 | + $prev_dd_payments = []; | |
| 102 | + if ($damage_deposit_payment['damagedep_gross'] ?? 0) { | |
| 103 | + $prev_dd_payments = VikBooking::getBookingHistoryInstance($booking['id']) | |
| 104 | + ->getEventsWithData('PN', function($data) { | |
| 105 | + return (is_object($data) && !empty($data->damage_deposit)); | |
| 106 | + }); | |
| 107 | + if (!$prev_dd_payments) { | |
| 108 | + // check also the first payment event in case of OTA bookings | |
| 109 | + $prev_dd_payments = VikBooking::getBookingHistoryInstance($booking['id']) | |
| 110 | + ->getEventsWithData('P0', function($data) { | |
| 111 | + return (is_object($data) && !empty($data->damage_deposit)); | |
| 112 | + }); | |
| 113 | + } | |
| 114 | + } | |
| 115 | + if ($prev_dd_payments && !empty($booking['tot_damage_dep'])) { | |
| 116 | + $remaining_bal -= $booking['tot_damage_dep']; | |
| 117 | + } | |
| 118 | + $content = str_replace('{remaining_balance}', VikBooking::numberFormat($remaining_bal), $content); | |
| 119 | + } | |
| 82 | 120 | |
| 83 | - return $layout; | |
| 84 | - } | |
| 121 | + if ($customer['pin'] ?? '') { | |
| 122 | + $content = str_replace('{customer_pin}', $customer['pin'], $content); | |
| 123 | + } | |
| 85 | 124 | |
| 86 | - /** | |
| 87 | - * Returns the default mail wrapper layout HTML code. | |
| 88 | - * | |
| 89 | - * @param bool $opening whether to get the opening or closing HTML. | |
| 90 | - * | |
| 91 | - * @return string | |
| 92 | - */ | |
| 93 | - private static function getWrapperDefaultLayout($opening = true) | |
| 94 | - { | |
| 95 | - $layout_opening = "\n"; | |
| 96 | - $layout_opening .= "<div style=\"background: #fdfdfd;padding: 30px 0;\">"; | |
| 97 | - $layout_opening .= "\n\t"; | |
| 98 | - $layout_opening .= "<div style=\"max-width: 600px;margin: 0 auto;background: #fff;padding: 30px;border: 1px solid #eee;border-radius: 6px\">"; | |
| 99 | - $layout_opening .= "\n"; | |
| 125 | + $use_sid = empty($booking['sid']) && !empty($booking['idorderota']) ? $booking['idorderota'] : $booking['sid']; | |
| 126 | + $bestitemid = VikBooking::findProperItemIdType(['booking'], (!empty($booking['lang']) ? $booking['lang'] : null)); | |
| 127 | + $lang_suffix = $bestitemid && !empty($booking['lang']) ? '&lang=' . $booking['lang'] : ''; | |
| 128 | + $book_link = VikBooking::externalroute("index.php?option=com_vikbooking&view=booking&sid=" . $use_sid . "&ts=" . $booking['ts'] . $lang_suffix, false, (!empty($bestitemid) ? $bestitemid : null)); | |
| 100 | 129 | |
| 101 | - $layout_closing = "\n"; | |
| 102 | - $layout_closing .= "\t</div>\n"; | |
| 103 | - $layout_closing .= "</div>\n"; | |
| 130 | + // access the model for shortening URLs | |
| 131 | + $model = VBOModelShortenurl::getInstance($onlyRouted = true)->setBooking($booking); | |
| 104 | 132 | |
| 105 | - return $opening ? $layout_opening : $layout_closing; | |
| 106 | - } | |
| 133 | + $content = str_replace('{booking_link}', $model->getShortUrl($book_link), $content); | |
| 134 | + | |
| 135 | + // rooms distinctive features parsing | |
| 136 | + preg_match_all('/\{roomfeature ([a-zA-Z0-9 ]+)\}/U', $content, $matches); | |
| 137 | + if (isset($matches[1]) && $matches[1]) { | |
| 138 | + foreach ($matches[1] as $reqf) { | |
| 139 | + $rooms_features = []; | |
| 140 | + foreach ($booking_rooms as $broom) { | |
| 141 | + $distinctive_features = []; | |
| 142 | + $rparams = !empty($broom['params']) && is_string($broom['params']) ? ((array) json_decode($broom['params'], true)) : []; | |
| 143 | + if (($rparams['features'] ?? []) && ($broom['roomindex'] ?? '') && ($rparams['features'][$broom['roomindex']] ?? [])) { | |
| 144 | + $distinctive_features = $rparams['features'][$broom['roomindex']]; | |
| 145 | + } | |
| 146 | + if (!$distinctive_features) { | |
| 147 | + continue; | |
| 148 | + } | |
| 149 | + $feature_found = false; | |
| 150 | + foreach ($distinctive_features as $dfk => $dfv) { | |
| 151 | + if (stripos($dfk, $reqf) !== false) { | |
| 152 | + $feature_found = $dfk; | |
| 153 | + if (strlen(trim($dfk)) == strlen(trim($reqf))) { | |
| 154 | + break; | |
| 155 | + } | |
| 156 | + } | |
| 157 | + } | |
| 158 | + if ($feature_found !== false && strlen((string) $distinctive_features[$feature_found])) { | |
| 159 | + $rooms_features[] = $distinctive_features[$feature_found]; | |
| 160 | + } | |
| 161 | + } | |
| 162 | + if ($rooms_features) { | |
| 163 | + $rpval = implode(', ', $rooms_features); | |
| 164 | + } else { | |
| 165 | + $rpval = ''; | |
| 166 | + } | |
| 167 | + $content = str_replace("{roomfeature " . $reqf . "}", $rpval, $content); | |
| 168 | + } | |
| 169 | + } | |
| 170 | + | |
| 171 | + /** | |
| 172 | + * Parse all Door Access Control tags. | |
| 173 | + * | |
| 174 | + * @since 1.18.4 (J) - 1.8.4 (WP) | |
| 175 | + */ | |
| 176 | + VBOFactory::getDoorAccessControl() | |
| 177 | + ->parseTokens((new VBOBookingRegistry($booking, $booking_rooms)), $content); | |
| 178 | + | |
| 179 | + return $content; | |
| 180 | + } | |
| 181 | + | |
| 182 | + /** | |
| 183 | + * Tells if a given string contains markdown syntax. | |
| 184 | + * | |
| 185 | + * @param string $text The text string to evaluate. | |
| 186 | + * | |
| 187 | + * @return bool True if markdown syntax is detected. | |
| 188 | + * | |
| 189 | + * @since 1.18.8 (J) - 1.8.8 (WP) | |
| 190 | + */ | |
| 191 | + public static function containsMarkdown(string $text) | |
| 192 | + { | |
| 193 | + $pattern = '/ | |
| 194 | + \*\*(.+?)\*\* | # Bold | |
| 195 | + (?<!\*)\*(?!\*)(.+?)(?<!\*)\*(?!\*) | # Italic | |
| 196 | + ~~(.+?)~~ | # Strikethrough | |
| 197 | + \[(.+?)\]\((.+?)\) | # Link | |
| 198 | + ^\#{1,3}\s | # Headings h1-h3 | |
| 199 | + ^\-\s | # Unordered list | |
| 200 | + ^\d+\.\s | # Ordered list | |
| 201 | + ^>\s # Blockquote | |
| 202 | + /xm'; | |
| 203 | + | |
| 204 | + return (bool) preg_match($pattern, $text); | |
| 205 | + } | |
| 206 | + | |
| 207 | + /** | |
| 208 | + * Attempts to convert markdown syntax into HTML from a given text string. | |
| 209 | + * | |
| 210 | + * @param string $text The text string to evaluate. | |
| 211 | + * | |
| 212 | + * @return string The manipulated string eventually containing HTML tags. | |
| 213 | + * | |
| 214 | + * @since 1.18.8 (J) - 1.8.8 (WP) | |
| 215 | + */ | |
| 216 | + public static function markdownToHtml(string $text) | |
| 217 | + { | |
| 218 | + if (!class_exists('VCMAiHelperMarkdown')) { | |
| 219 | + return $text; | |
| 220 | + } | |
| 221 | + | |
| 222 | + return (new VCMAiHelperMarkdown($text))->toHtml(); | |
| 223 | + } | |
| 224 | + | |
| 225 | + /** | |
| 226 | + * Checks for any content wrapper HTML tags applied through | |
| 227 | + * the Visual Editor during the composing of an email message. | |
| 228 | + * | |
| 229 | + * @param string $content the full email content. | |
| 230 | + * | |
| 231 | + * @return string | |
| 232 | + */ | |
| 233 | + public static function checkWrapperSymbols($content) | |
| 234 | + { | |
| 235 | + $chunks = preg_split(static::$wrapper_pattern, (string)$content); | |
| 236 | + | |
| 237 | + if (count($chunks) < 2) { | |
| 238 | + // no content wrapper used in email message | |
| 239 | + return $content; | |
| 240 | + } | |
| 241 | + | |
| 242 | + $layout_opening = self::getWrapperLayout(true); | |
| 243 | + $layout_closing = self::getWrapperLayout(false); | |
| 244 | + | |
| 245 | + $final_content = ''; | |
| 246 | + foreach ($chunks as $piece => $part) { | |
| 247 | + $is_even = ($piece === 0 || (($piece % 2) === 0)); | |
| 248 | + if (strlen(trim($part)) && !$is_even) { | |
| 249 | + $final_content .= $layout_opening . $part . $layout_closing; | |
| 250 | + } else { | |
| 251 | + $final_content .= $part; | |
| 252 | + } | |
| 253 | + } | |
| 254 | + | |
| 255 | + return $final_content; | |
| 256 | + } | |
| 257 | + | |
| 258 | + /** | |
| 259 | + * Returns the current mail wrapper layout HTML code. | |
| 260 | + * | |
| 261 | + * @param bool $opening whether to get the opening or closing HTML. | |
| 262 | + * | |
| 263 | + * @return string | |
| 264 | + */ | |
| 265 | + public static function getWrapperLayout($opening = true) | |
| 266 | + { | |
| 267 | + // configuration field name | |
| 268 | + $opt_name = 'mail_wrapper_layout_' . ($opening ? 'opening' : 'closing'); | |
| 269 | + | |
| 270 | + // access the configuration object | |
| 271 | + $config = VBOFactory::getConfig(); | |
| 272 | + | |
| 273 | + // get the current configuration value | |
| 274 | + $layout = $config->get($opt_name); | |
| 275 | + | |
| 276 | + if (!$layout) { | |
| 277 | + // set and get default layout | |
| 278 | + $layout = self::getWrapperDefaultLayout($opening); | |
| 279 | + $config->set($opt_name, $layout); | |
| 280 | + } | |
| 281 | + | |
| 282 | + return $layout; | |
| 283 | + } | |
| 284 | + | |
| 285 | + /** | |
| 286 | + * Returns the default mail wrapper layout HTML code. | |
| 287 | + * | |
| 288 | + * @param bool $opening whether to get the opening or closing HTML. | |
| 289 | + * | |
| 290 | + * @return string | |
| 291 | + */ | |
| 292 | + private static function getWrapperDefaultLayout($opening = true) | |
| 293 | + { | |
| 294 | + $layout_opening = "\n"; | |
| 295 | + $layout_opening .= "<div style=\"background: #fdfdfd;padding: 30px 0;\">"; | |
| 296 | + $layout_opening .= "\n\t"; | |
| 297 | + $layout_opening .= "<div style=\"max-width: 600px;margin: 0 auto;background: #fff;padding: 30px;border: 1px solid #eee;border-radius: 6px\">"; | |
| 298 | + $layout_opening .= "\n"; | |
| 299 | + | |
| 300 | + $layout_closing = "\n"; | |
| 301 | + $layout_closing .= "\t</div>\n"; | |
| 302 | + $layout_closing .= "</div>\n"; | |
| 303 | + | |
| 304 | + return $opening ? $layout_opening : $layout_closing; | |
| 305 | + } | |
| 107 | 306 | } |