(() => { // Shared script for: // - test-button-1.html: intercept "Check Rates" link and navigate to test-button-2.html // - test-button-2.html: inject ANTI-BOOK buttons next to each visible BOOK button const CHECK_RATES_OVERLAY_ID = "__ab_check_rates_overlay__"; const ANTI_MODAL_ID = "__anti_book_overlay__"; const ANTI_IFRAME_ID = "__anti_book_iframe__"; const ANTI_NOTICE_ID = "__anti_book_notice__"; const ANTI_BOOK_URL = "https://www.anti-booking.com/hotel_new/find.aspx?id_prov=5515&id_room=16802"; const ANTI_BOOK_API_AVAILABILITY = "https://www.anti-booking.com/Hotel_New/DataService.asmx/GetRoomsAvailability"; // Mirai UI classes vary across breakpoints; keep the base selector broad and filter in JS. const MIRAI_BOOK_BTN_SEL = 'ui-button.ex0cka_button,' + ' [is="ui-button"].ex0cka_button'; const normalizeText = (s) => (s || "").replace(/\s+/g, " ").trim(); const isProbablyRoomName = (text) => { if (!text) return false; const lower = text.toLowerCase(); if (!/[a-z]/i.test(text)) return false; if (lower.startsWith("+")) return false; if ( /\b(offer|rate|cancellation|policy|room only|last one|accommodation plan)\b/.test( lower, ) ) return false; if (/^\d+\s*(m|sqm|sq|square)\b/.test(lower)) return false; return true; }; const getMiraiRatesUrl = (lang) => { const entries = performance?.getEntriesByType?.("resource") || []; const matches = entries.filter((entry) => entry?.name?.includes("/booking/rates?"), ); const latest = matches[matches.length - 1]; if (!latest?.name) return ""; if (!lang) return latest.name; try { const url = new URL(latest.name); url.searchParams.set("lang", lang); return url.toString(); } catch { return latest.name; } }; const fetchMiraiRatesItems = async (lang) => { const cache = (window.__antiBookMiraiItems ||= {}); const cacheKey = lang || "default"; if (cache[cacheKey]) return cache[cacheKey]; const url = getMiraiRatesUrl(lang); if (!url) return null; const res = await fetch(url); if (!res.ok) return null; const json = await res.json(); const items = json?.data?.items; if (!Array.isArray(items)) return null; cache[cacheKey] = items; return items; }; const getMiraiRoomNameById = async (roomId, lang) => { const id = parsePositiveInt(roomId); if (!id) return ""; try { const items = await fetchMiraiRatesItems(lang); if (!items) return ""; const match = items.find((item) => parsePositiveInt(item?.id) === id); return normalizeText(match?.name || ""); } catch { return ""; } }; const isInsideSearchForm = (el) => { if (!el?.closest) return false; // Guardrail 1: skip search/finder forms (e.g. check-in/out controls) to avoid duplicate CTA injection. return Boolean( el.closest("ui-finder, [data-role=\"finder\"], [data-mirai-component=\"finder\"], form[data-search], [data-role=\"search\"], [data-mirai-component=\"search\"], form, [data-role=\"form\"], [data-mirai-component=\"form\"]") ); }; const isInsideSearchBar = (el) => { if (!el?.closest) return false; return Boolean( el.closest( "[data-role=\"filters\"], [data-role=\"search-bar\"], [data-role=\"searchbar\"], [data-role=\"refine-search\"], [data-search-bar], [data-mirai-component=\"filters\"], [data-mirai-component=\"search\"], [data-role=\"search\"]", ), ); }; const isInsidePopup = (el) => { if (!el?.closest) return false; // Guardrail 2: skip pop-ups/dialogs so we don't tamper with embedded modal CTAs. return Boolean( el.closest("[role=\"dialog\"], [data-role=\"dialog\"], [data-mirai-overlay], [data-modal], .mirai-popup, .modal, [data-popup]") ); }; const isMiraiPrimaryBookButton = (el) => { if (!el || el.nodeType !== 1) return false; const isUiButton = el.tagName === "UI-BUTTON" || el.getAttribute?.("is") === "ui-button"; if (!isUiButton) return false; if (isInsideSearchForm(el)) return false; if (isInsideSearchBar(el)) return false; if (isInsidePopup(el)) return false; // Never inject into the search / finder form UI. if (el.closest?.("ui-finder, [data-role=\"finder\"], [data-mirai-component=\"finder\"]")) return false; const classList = el.classList; if (!classList?.contains("ex0cka_button")) return false; const dataRole = el.getAttribute("data-role"); if (dataRole && dataRole !== "button") return false; if (dataRole === "finder-button") return false; if (el.closest?.('[data-anti-book-pair="1"]')) return false; const inRates = Boolean( el.closest?.('[data-mirai-component="rates"], [data-role="rates"]'), ); const hasRatesContainer = Boolean( document.querySelector('[data-mirai-component="rates"], [data-role="rates"]'), ); // Heuristic: booking CTA uses the primary button style. // Filter out known non-CTA variants (icons, small, secondary, transparent, disabled, etc.). const excluded = [ "ex0cka_small", "ex0cka_secondary", "ex0cka_transparent", "ex0cka_disabled", // Keep `ex0cka_large` because mobile CTAs use it. // NOTE: keep `ex0cka_squared`/`ex0cka_rounded` allowed; they appear on mobile CTAs. "TvyzcG_dispatcher", // icon-like dispatcher buttons (e.g. "Vista 3D") ]; for (const cls of excluded) if (classList.contains(cls)) return false; // Exclude +/- steppers and other icon-only buttons that carry aria-labels. const aria = normalizeText(el.getAttribute("aria-label") || "").toLowerCase(); if (aria === "add" || aria === "remove" || aria === "vista 3d") return false; // Exclude the search/find buttons (e.g. "Buscar") which reuse the same base classes. const containerText = normalizeText(el.closest?.('[data-mirai-component="rates"]')?.innerText || ""); if (!containerText) { // still proceed; some layouts hide text nodes } const elClasses = el.className || ""; if (/\bt5yToW_buttonSubmit\b/.test(elClasses) || /\bt5yToW_buttonAdd\b/.test(elClasses)) return false; if (hasRatesContainer && !inRates) return false; if (!hasRatesContainer) { const text = normalizeText(el.textContent).toLowerCase(); const label = normalizeText(el.getAttribute("aria-label") || "").toLowerCase(); const combined = `${text} ${label}`.trim(); if (!/\b(book|reserve|reserva|reservar|select)\b/.test(combined)) return false; } return true; }; const getOrigin = () => { // `location.origin` can be "null" for `file://` URLs. if (location.origin && location.origin !== "null") return location.origin; if (location.protocol && location.host) return `${location.protocol}//${location.host}`; return ""; }; const buildRedirectUrlFromHref = (href) => { const origin = getOrigin(); if (!origin) return ""; const dest = new URL("test-button-2.html", `${origin}${location.pathname || "/"}`); try { const be = new URL(href, location.href); dest.search = be.search || ""; } catch { dest.search = ""; } return dest.href; }; const ensureRedirectUrlsForCheckRatesLinks = () => { const links = Array.from( document.querySelectorAll('a.button[href]:not([data-redirect-url=""])'), ); for (const link of links) { if (link.hasAttribute("data-redirect-url")) continue; const redirectUrl = buildRedirectUrlFromHref(link.getAttribute("href") || ""); if (redirectUrl) link.setAttribute("data-redirect-url", redirectUrl); } }; const isVisible = (el) => { if (!el || el.nodeType !== 1) return false; const r = el.getBoundingClientRect(); if (!r || r.width === 0 || r.height === 0) return false; const style = getComputedStyle(el); return ( style.display !== "none" && style.visibility !== "hidden" && style.opacity !== "0" ); }; const shouldIgnoreEvent = (event) => { if (!event) return false; if (event.defaultPrevented) return true; if (event.button && event.button !== 0) return true; if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return true; return false; }; const cleanupRoommateParams = () => { if (!location.pathname.toLowerCase().endsWith("test-button-2.html")) return; const params = new URLSearchParams(location.search || ""); if (params.get("source") !== "roommate") return; const chainId = params.get("chainId"); if (!chainId) return; const cleanParams = new URLSearchParams(); cleanParams.set("source", "roommate"); cleanParams.set("chainId", chainId); if (params.get("finder") === "1") cleanParams.set("finder", "1"); const cleanUrl = `${location.pathname}?${cleanParams.toString()}`; const rawHref = location.href || ""; const rawSearch = location.search || ""; const hasExtraParams = /[?&](checkin|nights|parties|clientCode)=/i.test(rawHref); if (hasExtraParams) { location.replace(cleanUrl); return; } history.replaceState({}, "", cleanUrl); }; // -------- test-button-1.html: Check Rates redirect -------- const ensureCheckRatesOverlay = () => { let overlay = document.getElementById(CHECK_RATES_OVERLAY_ID); if (overlay) return overlay; overlay = document.createElement("div"); overlay.id = CHECK_RATES_OVERLAY_ID; overlay.style.position = "fixed"; overlay.style.inset = "0"; overlay.style.zIndex = "2147483647"; overlay.style.display = "none"; overlay.style.placeItems = "center"; overlay.style.background = "rgba(15, 18, 24, 0)"; overlay.style.backdropFilter = "blur(0px)"; overlay.style.transition = "background 140ms ease, backdrop-filter 140ms ease"; const panel = document.createElement("div"); panel.dataset.abOverlayPanel = "1"; panel.style.padding = "12px 14px"; panel.style.borderRadius = "12px"; panel.style.background = "rgba(17, 17, 17, 0.92)"; panel.style.color = "#fff"; panel.style.font = "600 14px system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif"; panel.style.boxShadow = "0 20px 60px rgba(0,0,0,0.45)"; panel.textContent = "Loading..."; overlay.appendChild(panel); document.documentElement.appendChild(overlay); return overlay; }; const hideCheckRatesOverlay = () => { const overlay = document.getElementById(CHECK_RATES_OVERLAY_ID); if (!overlay) return; overlay.style.display = "none"; overlay.style.background = "rgba(15, 18, 24, 0)"; overlay.style.backdropFilter = "blur(0px)"; }; const showCheckRatesOverlay = () => { const overlay = ensureCheckRatesOverlay(); const panel = overlay.querySelector('[data-ab-overlay-panel="1"]'); if (panel) panel.textContent = "Loading..."; overlay.style.display = "grid"; requestAnimationFrame(() => { overlay.style.background = "rgba(15, 18, 24, 0.55)"; overlay.style.backdropFilter = "blur(2px)"; }); }; const installCheckRatesRedirect = () => { ensureRedirectUrlsForCheckRatesLinks(); // When navigating back (including BFCache restores), hide the overlay if it was left visible. window.addEventListener("pageshow", hideCheckRatesOverlay); window.addEventListener("popstate", hideCheckRatesOverlay); document.addEventListener("visibilitychange", () => { if (!document.hidden) hideCheckRatesOverlay(); }); document.addEventListener( "click", (event) => { const rawTarget = event.target; const target = rawTarget instanceof Element ? rawTarget : rawTarget?.nodeType === 3 ? rawTarget.parentElement : null; const link = target?.closest?.("a.button[href]"); if (!link) return; if (shouldIgnoreEvent(event)) return; const redirectUrl = link.getAttribute("data-redirect-url") || buildRedirectUrlFromHref(link.getAttribute("href") || ""); if (!redirectUrl) return; link.setAttribute("data-redirect-url", redirectUrl); event.preventDefault(); event.stopPropagation(); event.stopImmediatePropagation(); showCheckRatesOverlay(); window.setTimeout(() => window.location.assign(redirectUrl), 160); }, true, ); }; // -------- test-button-2.html: ANTI-BOOK injection -------- const ensureAntiModal = () => { let overlay = document.getElementById(ANTI_MODAL_ID); if (overlay) return overlay; overlay = document.createElement("div"); overlay.id = ANTI_MODAL_ID; overlay.style.position = "fixed"; overlay.style.inset = "0"; overlay.style.background = "rgba(0,0,0,0.60)"; overlay.style.backdropFilter = "blur(2px)"; overlay.style.zIndex = "2147483647"; overlay.style.display = "none"; const panel = document.createElement("div"); panel.style.position = "absolute"; panel.style.inset = "24px"; panel.style.background = "#0b0b0b"; panel.style.borderRadius = "12px"; panel.style.boxShadow = "0 20px 60px rgba(0,0,0,0.5)"; panel.style.overflow = "hidden"; const bar = document.createElement("div"); bar.style.display = "flex"; bar.style.alignItems = "center"; bar.style.justifyContent = "space-between"; bar.style.gap = "12px"; bar.style.padding = "10px 12px"; bar.style.background = "#111"; bar.style.color = "#fff"; bar.style.font = "600 14px system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif"; const title = document.createElement("div"); title.textContent = "ANTI-BOOK"; const actions = document.createElement("div"); actions.style.display = "flex"; actions.style.gap = "8px"; const open = document.createElement("a"); open.textContent = "Open in new tab"; open.href = ANTI_BOOK_URL; open.target = "_blank"; open.rel = "noopener noreferrer"; open.style.color = "#cfcfcf"; open.style.textDecoration = "none"; open.style.fontWeight = "500"; const close = document.createElement("button"); close.type = "button"; close.textContent = "Close"; close.style.cursor = "pointer"; close.style.border = "1px solid rgba(255,255,255,0.25)"; close.style.background = "rgba(255,255,255,0.08)"; close.style.color = "#fff"; close.style.padding = "6px 10px"; close.style.borderRadius = "8px"; const iframe = document.createElement("iframe"); iframe.id = ANTI_IFRAME_ID; iframe.src = "about:blank"; iframe.style.width = "100%"; iframe.style.height = "calc(100% - 44px)"; iframe.style.border = "0"; const hide = () => { overlay.style.display = "none"; document.documentElement.style.overflow = ""; iframe.src = "about:blank"; }; close.addEventListener("click", hide); overlay.addEventListener("click", (e) => { if (e.target === overlay) hide(); }); window.addEventListener("keydown", (e) => { if (overlay.style.display !== "none" && e.key === "Escape") hide(); }); actions.appendChild(open); actions.appendChild(close); bar.appendChild(title); bar.appendChild(actions); panel.appendChild(bar); panel.appendChild(iframe); overlay.appendChild(panel); document.documentElement.appendChild(overlay); return overlay; }; const showAntiModal = () => { const overlay = ensureAntiModal(); const iframe = overlay.querySelector(`#${ANTI_IFRAME_ID}`); iframe.src = ANTI_BOOK_URL; overlay.style.display = "block"; document.documentElement.style.overflow = "hidden"; }; // -------- B1 -> B2: availability check + redirect -------- const ensureAntiLoadingOverlay = () => { let overlay = document.getElementById("__anti_book_loading__"); if (overlay) return overlay; overlay = document.createElement("div"); overlay.id = "__anti_book_loading__"; overlay.style.position = "fixed"; overlay.style.inset = "0"; overlay.style.zIndex = "2147483647"; overlay.style.display = "none"; overlay.style.placeItems = "center"; overlay.style.background = "rgba(15, 18, 24, 0.55)"; overlay.style.backdropFilter = "blur(2px)"; const panel = document.createElement("div"); panel.style.padding = "12px 14px"; panel.style.borderRadius = "12px"; panel.style.background = "rgba(17, 17, 17, 0.92)"; panel.style.color = "#fff"; panel.style.font = "600 14px system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif"; panel.style.boxShadow = "0 20px 60px rgba(0,0,0,0.45)"; panel.textContent = "Checking availability..."; overlay.appendChild(panel); document.documentElement.appendChild(overlay); return overlay; }; const showAntiLoadingOverlay = (message) => { const overlay = ensureAntiLoadingOverlay(); const panel = overlay.firstElementChild; if (panel && message) panel.textContent = message; overlay.style.display = "grid"; }; const hideAntiLoadingOverlay = () => { const overlay = document.getElementById("__anti_book_loading__"); if (overlay) overlay.style.display = "none"; }; const ensureAntiNotice = () => { let notice = document.getElementById(ANTI_NOTICE_ID); if (notice) return notice; notice = document.createElement("div"); notice.id = ANTI_NOTICE_ID; notice.style.position = "fixed"; notice.style.left = "0"; notice.style.top = "0"; notice.style.right = "auto"; notice.style.bottom = "auto"; notice.style.transform = "none"; notice.style.zIndex = "2147483647"; notice.style.display = "none"; notice.style.alignItems = "center"; notice.style.gap = "12px"; notice.style.padding = "12px 14px"; notice.style.borderRadius = "12px"; notice.style.background = "rgba(17, 17, 17, 0.95)"; notice.style.color = "#fff"; notice.style.font = "600 14px system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif"; notice.style.boxShadow = "0 20px 60px rgba(0,0,0,0.45)"; const message = document.createElement("div"); message.dataset.abNoticeMessage = "1"; message.textContent = "Room not available."; message.style.whiteSpace = "pre-line"; // allow explicit `\n` to wrap onto the next line const close = document.createElement("button"); close.type = "button"; close.textContent = "Close"; close.style.cursor = "pointer"; close.style.border = "1px solid rgba(255,255,255,0.25)"; close.style.background = "rgba(255,255,255,0.08)"; close.style.color = "#fff"; close.style.padding = "6px 10px"; close.style.borderRadius = "8px"; const destroy = () => { if (notice.__antiNoticeTimer) { clearTimeout(notice.__antiNoticeTimer); notice.__antiNoticeTimer = null; } notice.remove(); }; close.addEventListener("click", destroy); notice.appendChild(message); notice.appendChild(close); document.documentElement.appendChild(notice); return notice; }; const showAntiNotice = (message) => { const notice = ensureAntiNotice(); const messageEl = notice.querySelector('[data-ab-notice-message="1"]'); if (messageEl) messageEl.textContent = message || "Room not available."; notice.style.display = "flex"; if (notice.__antiAnchor) { const anchor = notice.__antiAnchor; const rect = anchor.getBoundingClientRect(); const spacing = 12; const noticeRect = notice.getBoundingClientRect(); const left = Math.max( 12, Math.min( rect.left + rect.width / 2 - noticeRect.width / 2, window.innerWidth - noticeRect.width - 12, ), ); const top = Math.max(12, rect.top - noticeRect.height - spacing); notice.style.left = `${left}px`; notice.style.top = `${top}px`; notice.style.transform = "none"; } else { notice.style.left = "50%"; notice.style.top = "50%"; notice.style.transform = "translate(-50%, -50%)"; } if (notice.__antiNoticeTimer) clearTimeout(notice.__antiNoticeTimer); notice.__antiNoticeTimer = setTimeout(() => { notice.remove(); }, 5000); }; const decodeParties = (value) => { if (!value) return []; try { return JSON.parse(atob(value)); } catch { return []; } }; const getPartyAdultsPerRoom = (parties) => { if (!Array.isArray(parties) || parties.length === 0) return null; let maxAdults = null; for (const party of parties) { const count = parsePositiveInt(party?.adults); if (count) maxAdults = maxAdults == null ? count : Math.max(maxAdults, count); } return maxAdults; }; const getPartyChildrenPerRoom = (parties) => { if (!Array.isArray(parties) || parties.length === 0) return null; let maxChildren = 0; let hasValue = false; for (const party of parties) { if (Array.isArray(party?.children)) { maxChildren = Math.max(maxChildren, party.children.length); hasValue = true; } } return hasValue ? maxChildren : null; }; const parseDate = (value) => { // Expected format: dd/MM/yyyy if (!value) return null; const [dd, mm, yyyy] = value.split("/").map((n) => parseInt(n, 10)); if (!dd || !mm || !yyyy) return null; return new Date(yyyy, mm - 1, dd); }; const formatDate = (date) => { if (!(date instanceof Date)) return ""; const dd = String(date.getDate()).padStart(2, "0"); const mm = String(date.getMonth() + 1).padStart(2, "0"); const yyyy = String(date.getFullYear()); return `${dd}/${mm}/${yyyy}`; }; const addDays = (date, days) => { if (!(date instanceof Date)) return null; const copy = new Date(date.getTime()); copy.setDate(copy.getDate() + days); return copy; }; const extractRoomName = (bookBtn) => { if (!bookBtn) return ""; const findRoomNameIn = (root) => { const explicit = root.querySelector( '[data-room-name], [data-role="room-name"], [data-mirai-component="room-name"]', ); const explicitText = normalizeText(explicit?.textContent || ""); if (explicitText) return explicitText; const headline = root.querySelector('ui-text[class*="headline-3"]'); const headlineText = normalizeText(headline?.textContent || ""); if (isProbablyRoomName(headlineText)) return headlineText; const heading = root.querySelector("h3, h4, h5, ui-text[variant*=\"heading\"]"); const headingText = normalizeText(heading?.textContent || ""); if (isProbablyRoomName(headingText)) return headingText; const fallback = Array.from( root.querySelectorAll('ui-text[class*="headline-"], ui-text[class*="title"]'), ) .map((el) => normalizeText(el.textContent)) .find((text) => isProbablyRoomName(text)); return fallback || ""; }; // Walk up to the nearest container that includes the room heading. let node = bookBtn; for (let i = 0; i < 10 && node; i += 1) { const candidate = findRoomNameIn(node); if (candidate) return candidate; node = node.parentElement; } return ""; }; const extractRoomId = (bookBtn) => { if (!bookBtn) return ""; const getRootsWithin = (root) => { const roots = []; const visited = new Set(); const queue = []; const add = (node) => { if (!node || visited.has(node)) return; visited.add(node); roots.push(node); queue.push(node); }; add(root); while (queue.length && roots.length < 100) { const node = queue.shift(); const nodes = node.querySelectorAll ? node.querySelectorAll("*") : []; for (const el of nodes) { const shadow = el.shadowRoot; if (shadow) add(shadow); } } return roots; }; const toRoomId = (value) => { const parsed = parsePositiveInt(value); return parsed ? String(parsed) : ""; }; const readRoomIdAttrs = (el, attrs) => { if (!el?.getAttribute) return ""; for (const attr of attrs) { const raw = el.getAttribute(attr); const parsed = toRoomId(raw); if (parsed) return parsed; } return ""; }; const readRoomIdFromHref = (value) => { if (!value) return ""; try { const url = new URL(value, location.href); return toRoomId(url.searchParams.get("id_room")); } catch { return ""; } }; const findRoomIdIn = (root) => { const roots = getRootsWithin(root); const attrs = [ "data-room-id", "data-roomid", "data-id-room", "data-id-roomid", "data-id_room", "data-room", "data-roomcode", "data-room-code", "data-id", "room_id", ]; for (const current of roots) { const direct = readRoomIdAttrs(current, attrs); if (direct) return direct; if (current?.querySelector) { for (const attr of attrs) { const el = current.querySelector(`[${attr}]`); const found = readRoomIdAttrs(el, [attr]); if (found) return found; } const hrefEl = current.querySelector( 'a[href*="id_room="], [href*="id_room="]', ); const href = hrefEl?.getAttribute?.("href") || ""; const fromHref = readRoomIdFromHref(href); if (fromHref) return fromHref; } } return ""; }; // Walk up to the nearest container that includes the room data. let node = bookBtn; for (let i = 0; i < 10 && node; i += 1) { const candidate = findRoomIdIn(node); if (candidate) return candidate; node = node.parentElement; } return ""; }; const extractB1RoomId = (bookBtn) => { if (!bookBtn) return ""; const getRootsWithin = (root) => { const roots = []; const visited = new Set(); const queue = []; const add = (node) => { if (!node || visited.has(node)) return; visited.add(node); roots.push(node); queue.push(node); }; add(root); while (queue.length && roots.length < 100) { const node = queue.shift(); const nodes = node.querySelectorAll ? node.querySelectorAll("*") : []; for (const el of nodes) { const shadow = el.shadowRoot; if (shadow) add(shadow); } } return roots; }; const findRateIdIn = (root) => { const roots = getRootsWithin(root); for (const current of roots) { const rateNodes = current.querySelectorAll ? current.querySelectorAll('[id^="Rate-"]') : []; for (const node of rateNodes) { const id = node.getAttribute?.("id") || ""; const match = id.match(/^Rate-(\d+)-/i); if (match) return match[1]; } const inputNodes = current.querySelectorAll ? current.querySelectorAll('input[name^="Rate "]') : []; for (const node of inputNodes) { const name = node.getAttribute?.("name") || ""; const match = name.match(/^Rate\s+(\d+)$/i); if (match) return match[1]; } const labeledNodes = current.querySelectorAll ? current.querySelectorAll('[aria-labelledby*="Rate-"]') : []; for (const node of labeledNodes) { const label = node.getAttribute?.("aria-labelledby") || ""; const match = label.match(/Rate-(\d+)-/i); if (match) return match[1]; } } return ""; }; let node = bookBtn; for (let i = 0; i < 10 && node; i += 1) { const candidate = findRateIdIn(node); if (candidate) return candidate; node = node.parentElement; } return ""; }; const parsePositiveInt = (value) => { const num = parseInt(value, 10); if (Number.isNaN(num) || num <= 0) return null; return num; }; const parseNonNegativeInt = (value) => { const num = parseInt(value, 10); if (Number.isNaN(num) || num < 0) return null; return num; }; const getParamInt = (params, keys, { allowZero = false } = {}) => { for (const key of keys) { const raw = params.get(key); if (raw == null || raw === "") continue; const parsed = parseInt(raw, 10); if (Number.isNaN(parsed)) continue; if (allowZero ? parsed >= 0 : parsed > 0) return parsed; } return null; }; const escapeSelectorValue = (value) => { if (!value) return ""; if (typeof CSS !== "undefined" && CSS.escape) return CSS.escape(value); return value.replace(/(["\\])/g, "\\$1"); }; const getElementValueByName = (name) => { if (!name || typeof document === "undefined") return null; const selector = `[name="${escapeSelectorValue(name)}"]`; const el = document.querySelector(selector); return el?.value || null; }; // DOM fallback so we read the actual room count that the user selected, even when the query string lags. const getRoomCountFromDom = () => { if (typeof document === "undefined") return null; const candidateNames = [ "FindForm1$selQuantity", "FindForm1_selQuantity", "selQuantity", "rooms", "room", "roomCount", "selRooms", ]; for (const name of candidateNames) { const parsed = parsePositiveInt(getElementValueByName(name)); if (parsed) return parsed; } const candidateIds = ["FindForm1_selQuantity", "FindForm1_selRooms"]; for (const id of candidateIds) { const el = document.getElementById(id); const parsed = parsePositiveInt(el?.value); if (parsed) return parsed; } const fuzzy = Array.from(document.querySelectorAll("select, input")).filter((el) => { const name = (el.getAttribute("name") || "").toLowerCase(); if (!name) return false; if (name.includes("child")) return false; if (!name.includes("room") && !name.includes("quantity")) return false; const tag = el.tagName?.toLowerCase(); if (tag === "input") { const type = (el.type || "").toLowerCase(); if (type === "checkbox" || type === "radio") return false; } return true; }); for (const el of fuzzy) { const parsed = parsePositiveInt(el.value); if (parsed) return parsed; } return null; }; const normalizeLangToken = (value) => { if (!value) return ""; let token = String(value).trim().toLowerCase(); try { token = token.normalize("NFD").replace(/[\u0300-\u036f]/g, ""); } catch {} token = token.replace(/_/g, "-").replace(/[^a-z-]/g, ""); return token; }; const getLangFromParams = (params) => { if (!params) return ""; const keys = ["lang", "language", "locale", "idioma", "lng"]; for (const key of keys) { const value = params.get(key); if (value) return value; } return ""; }; const getLangFromMeta = () => { if (typeof document === "undefined") return ""; const meta = document.querySelector( 'meta[name="language"], meta[http-equiv="content-language"], meta[property="og:locale"]', ); return meta?.content || ""; }; const getLangFromDom = () => { if (typeof document === "undefined") return ""; return ( document.documentElement?.lang || document.body?.lang || document.documentElement?.getAttribute?.("lang") || "" ); }; const getLangFromDataset = () => { if (typeof document === "undefined") return ""; const nodes = [document.documentElement, document.body]; for (const node of nodes) { if (!node?.dataset) continue; const value = node.dataset.lang || node.dataset.language || node.dataset.locale; if (value) return value; } const el = document.querySelector("[data-lang], [data-language], [data-locale]"); if (!el) return ""; return ( el.getAttribute("data-lang") || el.getAttribute("data-language") || el.getAttribute("data-locale") || "" ); }; const getLangFromSelect = () => { if (typeof document === "undefined") return ""; const select = document.querySelector( 'select[name*="lang" i], select[id*="lang" i], select[name*="language" i], select[id*="language" i], select[name*="locale" i], select[id*="locale" i]', ); if (!select) return ""; const value = select.value || ""; if (value) return value; const selected = select.selectedOptions?.[0]; return selected?.value || selected?.textContent || ""; }; const pickLanguage = (values) => { const normalized = values .map((value) => normalizeLangToken(value)) .filter(Boolean); if (!normalized.length) return ""; const nonEnglish = normalized.find((value) => !value.startsWith("en")); return nonEnglish || normalized[0]; }; const resolveB1Language = (params) => { const paramLang = getLangFromParams(params); const selectLang = getLangFromSelect(); const datasetLang = getLangFromDataset(); const domLang = getLangFromDom(); const metaLang = getLangFromMeta(); const resolved = pickLanguage([ selectLang, datasetLang, domLang, metaLang, paramLang, ]); if (resolved) return resolved; const navigatorLang = typeof navigator !== "undefined" ? navigator.language || navigator.userLanguage : ""; return navigatorLang || "en"; }; const mapLanguage = (lang) => { // Destination BE uses lowercase labels like "english". const normalized = normalizeLangToken(lang); if (!normalized) return "english"; if (normalized.startsWith("en") || normalized === "english") return "english"; if ( normalized.startsWith("es") || normalized === "spanish" || normalized === "espanol" || normalized === "castellano" ) return "spanish"; if ( normalized.startsWith("fr") || normalized === "french" || normalized === "francais" ) return "french"; if ( normalized.startsWith("it") || normalized === "italian" || normalized === "italiano" ) return "italian"; if ( normalized.startsWith("de") || normalized === "german" || normalized === "deutsch" ) return "german"; return "english"; }; const buildDestinationUrl = ({ idProv, idRoom, checkIn, nights, adults, children, rooms, lang, currency, roomName, returnUrl, }) => { const checkInDate = parseDate(checkIn); const checkOutDate = addDays(checkInDate, nights || 1); const params = new URLSearchParams(); params.set("id_prov", String(idProv)); if (idRoom) params.set("id_room", String(idRoom)); params.set("check_in", formatDate(checkInDate)); params.set("check_out", formatDate(checkOutDate)); params.set("nights", String(nights || 1)); const resolvedAdults = parsePositiveInt(adults) || 2; const resolvedChildren = parseNonNegativeInt(children) ?? 0; params.set("adults", String(resolvedAdults)); params.set("FindForm1$selAdults", String(resolvedAdults)); params.set("FindForm1_selAdults", String(resolvedAdults)); params.set("selAdults", String(resolvedAdults)); params.set("children", String(resolvedChildren)); params.set("child1", String(resolvedChildren)); params.set("child2", "0"); params.set("child3", "0"); params.set("FindForm1$selChild1", String(resolvedChildren)); params.set("FindForm1_selChild1", String(resolvedChildren)); params.set("selChild1", String(resolvedChildren)); params.set("FindForm1$selChild2", "0"); params.set("FindForm1_selChild2", "0"); params.set("selChild2", "0"); params.set("FindForm1$selChild3", "0"); params.set("FindForm1_selChild3", "0"); params.set("selChild3", "0"); const parsedRooms = parseInt(rooms || "1", 10); const resolvedRooms = Number.isFinite(parsedRooms) && parsedRooms > 0 ? parsedRooms : 1; params.set("rooms", String(resolvedRooms)); params.set("room", String(resolvedRooms)); params.set("roomCount", String(resolvedRooms)); params.set("quantity", String(resolvedRooms)); params.set("FindForm1$selQuantity", String(resolvedRooms)); params.set("FindForm1_selQuantity", String(resolvedRooms)); params.set("selQuantity", String(resolvedRooms)); params.set("FindForm1$selRooms", String(resolvedRooms)); params.set("FindForm1_selRooms", String(resolvedRooms)); params.set("selRooms", String(resolvedRooms)); const mappedLang = mapLanguage(lang); params.set("language", mappedLang); params.set("lang", mappedLang); params.set("lstLanguages", mappedLang); params.set("LanguageSwitcher1$chosen_lang", mappedLang); params.set("LanguageSwitcher1$lstLanguages", mappedLang); if (currency) params.set("currency", currency); if (roomName) params.set("room_name", roomName); if (returnUrl) params.set("beUrl", returnUrl); return `https://www.anti-booking.com/hotel_new/find.aspx?${params.toString()}`; }; const checkAvailability = async ({ start, nights, adults, children, idProv, roomId, roomDescription, roomEnable, quantity, mealPlan, promoId, useRoomFilter, }) => { const childCount = Math.max(0, parseInt(children || 0, 10) || 0); const qty = Math.max(1, parseInt(quantity || 1, 10) || 1); const mPlan = parseInt(mealPlan || 0, 10) || 0; const promo = parseInt(promoId || 0, 10) || 0; const payload = { start, nights, adults, children: childCount, id_prov: idProv, online_id: 0, roomid: parsePositiveInt(roomId) || 0, room_description: roomDescription || null, room_enable: roomEnable || null, quantity: qty, child1: childCount, child2: 0, child3: 0, mealPlan: mPlan, discount: "0", promo_id: promo, use_room_filter: useRoomFilter !== false, }; const res = await fetch(ANTI_BOOK_API_AVAILABILITY, { method: "POST", headers: { "Content-Type": "application/json; charset=utf-8" }, body: JSON.stringify(payload), }); const json = await res.json(); const data = json?.d ? JSON.parse(json.d) : null; if (!data) return { ok: false, reason: "Empty availability response" }; const table = data.table || {}; const requestedStart = parseDate(start); const requestedNights = Math.max(1, parseInt(nights || "1", 10) || 1); const requestedDates = []; if (requestedStart) { for (let offset = 0; offset < requestedNights; offset += 1) { const currentDate = addDays(requestedStart, offset); const formattedDate = formatDate(currentDate); if (formattedDate) requestedDates.push(formattedDate); } } const tableGroups = Object.values(table); const roomIds = new Set(); const wantsNameMatch = isProbablyRoomName(roomDescription); const normalizedRoomName = wantsNameMatch ? normalizeText(roomDescription).toLowerCase() : ""; const roomNameMatches = new Map(); for (const group of tableGroups) { if (!group) continue; for (const entries of Object.values(group)) { if (!entries) continue; for (const [key, entry] of Object.entries(entries)) { if (key) roomIds.add(String(key)); if (!wantsNameMatch || !entry) continue; const entryName = normalizeText(entry.roomname || "").toLowerCase(); if (!entryName || entryName !== normalizedRoomName) continue; const priority = Number(entry.Priority); const rank = Number.isFinite(priority) ? priority : 9999; const existing = roomNameMatches.get(key); if (!existing || rank < existing.priority) { roomNameMatches.set(key, { priority: rank }); } } } } const hasAvailabilityForDate = (dateKey) => { for (const group of tableGroups) { const entries = group?.[dateKey]; if (!entries) continue; for (const entry of Object.values(entries)) { if (!entry) continue; const count = Number(entry.count) || 0; const rate = Number(entry.rate) || 0; const totalCost = Number(entry.totalcost) || 0; const hasPrice = rate > 0 || totalCost > 0; if (count > 0 && hasPrice) return true; } } return false; }; const allDatesAvailable = requestedDates.length === requestedNights && requestedDates.every((dateKey) => hasAvailabilityForDate(dateKey)); // Guardrail: rely on the API’s per-date table instead of trusting RoomAvail when only part of the range is filled. const canBook = Boolean(data?.RoomAvail?.RoomAvail) && (requestedDates.length === requestedNights ? allDatesAvailable : true); const roomProfileCreated = typeof data?.RoomProfileCreated === "boolean" ? data.RoomProfileCreated : null; const roomEnabled = typeof data?.RoomEnabled === "boolean" ? data.RoomEnabled : null; const resolvedRoomId = parsePositiveInt(roomId); const normalizedRoomId = resolvedRoomId ? String(resolvedRoomId) : ""; let matchedRoomId = ""; if (normalizedRoomId && roomIds.has(normalizedRoomId)) { matchedRoomId = normalizedRoomId; } else if (wantsNameMatch && roomNameMatches.size) { let bestId = ""; let bestPriority = Number.POSITIVE_INFINITY; for (const [id, meta] of roomNameMatches.entries()) { if (meta.priority < bestPriority) { bestPriority = meta.priority; bestId = String(id); } } matchedRoomId = bestId; } else if (roomIds.size === 1) { matchedRoomId = Array.from(roomIds)[0]; } return { ok: canBook, roomProfileCreated, roomEnabled, matchedRoomId }; }; const handleAntiBookClick = async (bookBtn) => { // This runs immediately after the ANTI-BOOK button is clicked on B1. // It checks availability via API and only then builds the B2 URL. try { showAntiLoadingOverlay("Checking availability..."); const notice = document.getElementById(ANTI_NOTICE_ID); if (notice) notice.__antiAnchor = bookBtn; const params = new URLSearchParams(location.search || ""); const checkIn = params.get("checkin") || ""; const nights = parseInt(params.get("nights") || "1", 10) || 1; const parties = decodeParties(params.get("parties")); const partyAdults = getPartyAdultsPerRoom(parties); const partyChildren = getPartyChildrenPerRoom(parties); const paramAdults = getParamInt(params, [ "adults", "adult", "no_of_adults", "FindForm1$selAdults", "FindForm1_selAdults", "selAdults", ]); const paramChildren = getParamInt( params, [ "children", "child", "kids", "child1", "FindForm1$selChild1", "FindForm1_selChild1", "selChild1", ], { allowZero: true }, ); const paramRooms = getParamInt(params, [ "rooms", "room", "roomCount", "quantity", "FindForm1$selQuantity", "FindForm1_selQuantity", "selQuantity", "FindForm1$selRooms", "FindForm1_selRooms", "selRooms", ]); const domRooms = getRoomCountFromDom(); const hasParamAdults = typeof paramAdults === "number" && paramAdults > 0; const hasPartyAdults = typeof partyAdults === "number" && partyAdults > 0; const adults = hasParamAdults ? paramAdults : hasPartyAdults ? partyAdults : 2; const adultsSource = hasParamAdults ? "query" : hasPartyAdults ? "parties" : "default"; const hasParamChildren = paramChildren !== null; const hasPartyChildren = partyChildren !== null; const children = hasParamChildren ? paramChildren : hasPartyChildren ? partyChildren : 0; const childrenSource = hasParamChildren ? "query" : hasPartyChildren ? "parties" : "default"; const roomsFromParties = Array.isArray(parties) && parties.length > 0 ? parties.length : null; const hasRoomsFromDom = typeof domRooms === "number" && domRooms > 0; const hasRoomsFromParam = typeof paramRooms === "number" && paramRooms > 0; const resolvedRooms = hasRoomsFromDom ? domRooms : hasRoomsFromParam ? paramRooms : roomsFromParties || 1; const roomsSource = hasRoomsFromDom ? "dom" : hasRoomsFromParam ? "query" : roomsFromParties ? "parties" : "default"; const rooms = resolvedRooms > 0 ? resolvedRooms : 1; const missingGuestDetails = []; if (roomsSource === "default") missingGuestDetails.push("rooms (default 1)"); if (adultsSource === "default") missingGuestDetails.push("adults (default 2)"); if (childrenSource === "default") missingGuestDetails.push("children (default 0)"); if (missingGuestDetails.length) { showAntiNotice( `Missing guest params from B1: ${missingGuestDetails.join( ", ", )}. Using fallback values.`, ); } const lang = resolveB1Language(params); const returnUrl = params.get("beUrl") || params.get("beurl") || params.get("returnUrl") || params.get("return") || location.href; const currency = params.get("currency") || "EUR"; // B1 = source BE (Mirai). B2 = destination BE (anti-booking). const resolvedRoomNameRaw = bookBtn.dataset.roomDescription || extractRoomName(bookBtn); if (resolvedRoomNameRaw && !bookBtn.dataset.roomDescription) { bookBtn.dataset.roomDescription = resolvedRoomNameRaw; } const resolvedRoomId = bookBtn.dataset.roomId || extractRoomId(bookBtn); if (resolvedRoomId && !bookBtn.dataset.roomId) { bookBtn.dataset.roomId = resolvedRoomId; } const resolvedB1RoomId = bookBtn.dataset.b1RoomId || extractB1RoomId(bookBtn); if (resolvedB1RoomId && !bookBtn.dataset.b1RoomId) { bookBtn.dataset.b1RoomId = resolvedB1RoomId; } const resolvedRoomName = (resolvedB1RoomId && (await getMiraiRoomNameById(resolvedB1RoomId, "en"))) || resolvedRoomNameRaw; if (!resolvedRoomName) { hideAntiLoadingOverlay(); showAntiNotice("Room name not found. Please try again."); return; } if (!resolvedB1RoomId) { hideAntiLoadingOverlay(); showAntiNotice( "Room id missing on B1. Please select the room again.", ); return; } const b1 = { b1_checkin: checkIn, b1_nights: nights, b1_adults: adults, b1_children: children, b1_rooms: rooms, b1_lang: lang, b1_currency: currency, b1_room_description: resolvedRoomName, b1_room_id: resolvedB1RoomId, }; const b2 = { b2_id_prov: 5515, b2_online_id: 0, b2_room_enable: "Y", b2_checkin: b1.b1_checkin, b2_nights: b1.b1_nights, b2_adults: b1.b1_adults, b2_children: b1.b1_children, b2_rooms: b1.b1_rooms, b2_lang: b1.b1_lang, b2_currency: b1.b1_currency, b2_room_description: b1.b1_room_id, b2_room_description_fallback: b1.b1_room_description, b2_room_name: b1.b1_room_description, b2_room_id: resolvedRoomId || "", }; const availability = await checkAvailability({ start: b2.b2_checkin, nights: b2.b2_nights, adults: b2.b2_adults, children: b2.b2_children, idProv: b2.b2_id_prov, roomId: b2.b2_room_id, roomDescription: b2.b2_room_description, roomEnable: b2.b2_room_enable, quantity: b2.b2_rooms, mealPlan: 0, promoId: 0, useRoomFilter: true, }); let resolvedAvailability = availability; let fallbackAvailability = null; if (!resolvedAvailability.ok || !resolvedAvailability.matchedRoomId) { fallbackAvailability = await checkAvailability({ start: b2.b2_checkin, nights: b2.b2_nights, adults: b2.b2_adults, children: b2.b2_children, idProv: b2.b2_id_prov, roomId: b2.b2_room_id, roomDescription: b2.b2_room_description_fallback, roomEnable: b2.b2_room_enable, quantity: b2.b2_rooms, mealPlan: 0, promoId: 0, useRoomFilter: true, }); } if ( fallbackAvailability && (fallbackAvailability.ok || fallbackAvailability.matchedRoomId || fallbackAvailability.roomProfileCreated !== null || fallbackAvailability.roomEnabled !== null) ) { resolvedAvailability = fallbackAvailability; } if (!resolvedAvailability.ok) { hideAntiLoadingOverlay(); if (resolvedAvailability.roomProfileCreated === false) { showAntiNotice( "Back-end not configured: Room Profile (B2) missing. Try again later.", ); } else if ( resolvedAvailability.roomProfileCreated === true && resolvedAvailability.roomEnabled === false ) { showAntiNotice("Room Profile (B2) exists but is disabled."); } else { showAntiNotice("Not available rooms for selected dates."); } return; } const matchedRoomId = resolvedAvailability.matchedRoomId || b2.b2_room_id; if (!matchedRoomId) { hideAntiLoadingOverlay(); showAntiNotice( "Room id mismatch between B1 and B2. Please select the room again.", ); return; } b2.b2_room_id = matchedRoomId; const destinationUrl = buildDestinationUrl({ idProv: b2.b2_id_prov, idRoom: b2.b2_room_id, checkIn: b2.b2_checkin, nights: b2.b2_nights, adults: b2.b2_adults, children: b2.b2_children, rooms: b2.b2_rooms, lang: b2.b2_lang, currency: b2.b2_currency, roomName: b2.b2_room_name, returnUrl, }); window.location.assign(destinationUrl); } catch (err) { hideAntiLoadingOverlay(); showAntiNotice("Availability check failed. Please try again."); } }; const setAntiLabel = (btn) => { const nodes = [btn, ...Array.from(btn.querySelectorAll?.("*") || [])]; for (const el of nodes) { const t = normalizeText(el.textContent); if (t && t.toLowerCase() === "book") { el.textContent = "ANTI-BOOK"; return; } } btn.textContent = "ANTI-BOOK"; }; const bindAnti = (btn) => { if (btn.__antiBookBound) return; const handler = (e) => { e.preventDefault?.(); e.stopPropagation?.(); e.stopImmediatePropagation?.(); // Run availability check and build the destination URL before navigating to B2. // This ensures the selected room is available for the chosen dates. handleAntiBookClick(btn); }; btn.addEventListener("click", handler, true); btn.addEventListener( "keydown", (e) => { if (e.key === "Enter" || e.key === " ") handler(e); }, true, ); btn.__antiBookBound = true; }; const insertAntiNextTo = (bookBtn) => { if (!bookBtn?.parentElement) return; if (bookBtn.closest?.('[data-anti-book-pair="1"]')) return; const wrapper = document.createElement("div"); wrapper.dataset.antiBookPair = "1"; wrapper.style.display = "flex"; wrapper.style.gap = "3px"; wrapper.style.alignItems = "center"; const antiBtn = bookBtn.cloneNode(true); antiBtn.setAttribute("aria-label", "ANTI-BOOK"); setAntiLabel(antiBtn); const roomTitle = extractRoomName(bookBtn); if (roomTitle) antiBtn.dataset.roomDescription = roomTitle; const roomId = extractRoomId(bookBtn); if (roomId) antiBtn.dataset.roomId = roomId; const b1RoomId = extractB1RoomId(bookBtn); if (b1RoomId) antiBtn.dataset.b1RoomId = b1RoomId; antiBtn.style.margin = "0"; bookBtn.style.margin = "0"; bookBtn.parentElement.insertBefore(wrapper, bookBtn); wrapper.appendChild(antiBtn); wrapper.appendChild(bookBtn); bindAnti(antiBtn); }; const ensureAntiEverywhere = () => { const bookButtons = Array.from(document.querySelectorAll(MIRAI_BOOK_BTN_SEL)) .filter(isVisible) .filter(isMiraiPrimaryBookButton); for (const bookBtn of bookButtons) insertAntiNextTo(bookBtn); }; const installAntiObserver = () => { ensureAntiEverywhere(); if (window.__antiBookObserverInstalled) return; window.__antiBookObserverInstalled = true; const scheduleRecheck = (delay = 200) => { const attempts = [delay, 600, 1200, 2000]; attempts.forEach((ms) => { setTimeout(() => { try { ensureAntiEverywhere(); } catch {} }, ms); }); }; let t = null; const obs = new MutationObserver(() => { clearTimeout(t); t = setTimeout(() => { try { ensureAntiEverywhere(); } catch {} }, 50); }); obs.observe(document.documentElement, { childList: true, subtree: true }); document.addEventListener( "click", (event) => { const rawTarget = event.target; const target = rawTarget instanceof Element ? rawTarget : rawTarget?.nodeType === 3 ? rawTarget.parentElement : null; if (!target) return; const searchBtn = target.closest?.( '[data-role="finder-button"], [data-role="search"], button[type="submit"], ui-button[aria-label="Search"]', ); if (!searchBtn) { const text = normalizeText(target.textContent).toLowerCase(); if (text !== "search") return; } scheduleRecheck(); }, true, ); document.addEventListener( "submit", () => { scheduleRecheck(300); }, true, ); }; const onThisPage = (name) => location.pathname.toLowerCase().endsWith(name); cleanupRoommateParams(); if (onThisPage("test-button-1.html")) installCheckRatesRedirect(); if (onThisPage("test-button-2.html")) { try { const params = new URLSearchParams(location.search || ""); const id = params.get("idtokenprovider"); if (id) { const mount = document.querySelector("[data-mirai-id]"); if (mount) mount.setAttribute("data-mirai-id", id); } } catch {} installAntiObserver(); if (window.Mirai?.Event?.subscribe) { try { window.Mirai.Event.subscribe("CORE_READY", () => { installAntiObserver(); }); } catch {} } } })();