PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.4
Yatra – Travel Booking & Tour Operator Software v3.0.4
3.0.16 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 All 84 releases
yatra / resources / js / pages / account / utils / downloads.ts

downloads.ts in Yatra – Travel Booking & Tour Operator Software 3.0.4, at resources/js/pages/account/utils/downloads.ts

430 lines 12.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 import { __ } from "../../../lib/i18n";
2
3 /**
4 * Account PDFs use Yatra REST routes with cookie auth + X-WP-Nonce.
5 * Plain-permalink sites use index.php?rest_route=/yatra/v1 — query params must
6 * stay on the URL, not inside rest_route.
7 */
8
9 type RestConfig = { base: string; nonce: string };
10
11 function getAccountRestConfig(): RestConfig {
12 if (typeof window === "undefined") {
13 return { base: "/wp-json/yatra/v1", nonce: "" };
14 }
15 const w = window as unknown as {
16 yatraAccountPage?: { apiUrl?: string; nonce?: string };
17 yatraAdmin?: { apiUrl?: string; nonce?: string };
18 };
19 const raw =
20 w.yatraAccountPage?.apiUrl || w.yatraAdmin?.apiUrl || "/wp-json/yatra/v1";
21 const base = String(raw).replace(/\/$/, "");
22 const nonce = w.yatraAccountPage?.nonce || w.yatraAdmin?.nonce || "";
23 return { base, nonce };
24 }
25
26 function parseYatraRestSubpath(href: string): string {
27 const origin =
28 typeof window !== "undefined" ? window.location.origin : "http://localhost";
29 try {
30 const u = new URL(href, origin);
31 const rr = u.searchParams.get("rest_route");
32 if (rr) {
33 const decoded = decodeURIComponent(rr.replace(/\+/g, " "));
34 const idx = decoded.indexOf("/yatra/v1");
35 if (idx !== -1) {
36 return decoded.slice(idx + "/yatra/v1".length) || decoded;
37 }
38 return decoded;
39 }
40 const path = u.pathname || "";
41 const marker = "/yatra/v1";
42 const pos = path.indexOf(marker);
43 if (pos !== -1) {
44 return path.slice(pos + marker.length);
45 }
46 } catch {
47 /* ignore */
48 }
49 return "";
50 }
51
52 function parsePaymentIdFromHref(href: string): number | null {
53 const sub = parseYatraRestSubpath(href);
54 const m = sub.match(/^\/payment\/(\d+)\/invoice(?:\/?|$)/i);
55 return m ? parseInt(m[1], 10) : null;
56 }
57
58 function parseBookingDocFromHref(
59 href: string,
60 kind: "voucher" | "itinerary",
61 ): number | null {
62 const sub = parseYatraRestSubpath(href);
63 const re = new RegExp(`^/bookings/(\\d+)/${kind}(?:/?|$)`, "i");
64 const m = sub.match(re);
65 return m ? parseInt(m[1], 10) : null;
66 }
67
68 /**
69 * Build GET URL for booking PDF endpoints.
70 */
71 function buildBookingDocumentUrl(
72 baseRaw: string,
73 bookingId: number,
74 kind: "voucher" | "itinerary",
75 mode: "download" | "preview" = "download",
76 ): string {
77 const suffix = `/bookings/${bookingId}/${kind}`;
78 const origin =
79 typeof window !== "undefined" ? window.location.origin : "http://localhost";
80
81 let u: URL;
82 try {
83 u = new URL(String(baseRaw).trim(), origin);
84 } catch {
85 const b = String(baseRaw).replace(/\/$/, "");
86 const q = mode === "download" ? "?download=1" : "?preview=1";
87 return `${b}${suffix}${q}`;
88 }
89
90 if (u.searchParams.has("rest_route")) {
91 const route = (u.searchParams.get("rest_route") || "").replace(/\/$/, "");
92 u.searchParams.set("rest_route", `${route}${suffix}`);
93 if (mode === "download") {
94 u.searchParams.set("download", "1");
95 u.searchParams.delete("preview");
96 } else {
97 u.searchParams.set("preview", "1");
98 u.searchParams.delete("download");
99 }
100 return u.toString();
101 }
102
103 u.pathname = (u.pathname || "").replace(/\/$/, "") + suffix;
104 if (mode === "download") {
105 u.searchParams.set("download", "1");
106 u.searchParams.delete("preview");
107 } else {
108 u.searchParams.set("preview", "1");
109 u.searchParams.delete("download");
110 }
111 return u.toString();
112 }
113
114 function buildPaymentInvoiceUrl(
115 baseRaw: string,
116 paymentId: number,
117 mode: "download" | "preview",
118 ): string {
119 const suffix = `/payment/${paymentId}/invoice`;
120 const origin =
121 typeof window !== "undefined" ? window.location.origin : "http://localhost";
122
123 let u: URL;
124 try {
125 u = new URL(String(baseRaw).trim(), origin);
126 } catch {
127 const b = String(baseRaw).replace(/\/$/, "");
128 const q = mode === "download" ? "?download=1" : "?preview=1";
129 return `${b}${suffix}${q}`;
130 }
131
132 if (u.searchParams.has("rest_route")) {
133 const route = (u.searchParams.get("rest_route") || "").replace(/\/$/, "");
134 u.searchParams.set("rest_route", `${route}${suffix}`);
135 if (mode === "download") {
136 u.searchParams.set("download", "1");
137 u.searchParams.delete("preview");
138 } else {
139 u.searchParams.set("preview", "1");
140 u.searchParams.delete("download");
141 }
142 return u.toString();
143 }
144
145 u.pathname = (u.pathname || "").replace(/\/$/, "") + suffix;
146 if (mode === "download") {
147 u.searchParams.set("download", "1");
148 u.searchParams.delete("preview");
149 } else {
150 u.searchParams.set("preview", "1");
151 u.searchParams.delete("download");
152 }
153 return u.toString();
154 }
155
156 async function readFetchErrorMessage(res: Response): Promise<string> {
157 let msg = res.statusText || __("Request failed", "yatra");
158 try {
159 const j = await res.json();
160 if (j?.message) {
161 msg = typeof j.message === "string" ? j.message : msg;
162 }
163 } catch {
164 const t = await res.text().catch(() => "");
165 if (t && t.length < 200) {
166 msg = t;
167 }
168 }
169 return msg;
170 }
171
172 async function fetchPreviewPdf(url: string): Promise<Blob> {
173 const { nonce } = getAccountRestConfig();
174 if (!nonce) {
175 throw new Error(
176 __(
177 "Missing security token. Reload the account page and try again.",
178 "yatra",
179 ),
180 );
181 }
182 const res = await fetch(url, {
183 method: "GET",
184 credentials: "include",
185 headers: {
186 "X-WP-Nonce": nonce,
187 Accept: "application/json",
188 },
189 });
190 if (!res.ok) {
191 throw new Error(await readFetchErrorMessage(res));
192 }
193 const data = (await res.json()) as {
194 pdf_data?: string;
195 filename?: string;
196 };
197 if (!data?.pdf_data || typeof data.pdf_data !== "string") {
198 throw new Error(__("Invalid preview response from the server.", "yatra"));
199 }
200 const binary = atob(data.pdf_data);
201 const bytes = new Uint8Array(binary.length);
202 for (let i = 0; i < binary.length; i++) {
203 bytes[i] = binary.charCodeAt(i);
204 }
205 return new Blob([bytes], { type: "application/pdf" });
206 }
207
208 function openPdfInNewTab(blob: Blob): void {
209 const objectUrl = URL.createObjectURL(blob);
210 const win = window.open(objectUrl, "_blank", "noopener,noreferrer");
211 if (!win) {
212 URL.revokeObjectURL(objectUrl);
213 throw new Error(
214 __(
215 "Popup blocked. Allow popups for this site to preview the PDF.",
216 "yatra",
217 ),
218 );
219 }
220 window.setTimeout(() => URL.revokeObjectURL(objectUrl), 60_000);
221 }
222
223 async function downloadBookingBinary(
224 bookingId: number,
225 kind: "voucher" | "itinerary",
226 ): Promise<void> {
227 const { base, nonce } = getAccountRestConfig();
228 if (!nonce) {
229 throw new Error(
230 __(
231 "Missing security token. Reload the account page and try again.",
232 "yatra",
233 ),
234 );
235 }
236 const url = buildBookingDocumentUrl(base, bookingId, kind, "download");
237 const res = await fetch(url, {
238 method: "GET",
239 credentials: "include",
240 headers: {
241 "X-WP-Nonce": nonce,
242 Accept: "application/pdf, application/octet-stream, */*",
243 },
244 });
245 if (!res.ok) {
246 throw new Error(await readFetchErrorMessage(res));
247 }
248 const blob = await res.blob();
249 const dispo = res.headers.get("Content-Disposition");
250 let filename = `${kind}-${bookingId}.pdf`;
251 if (dispo) {
252 const m = /filename\*?=(?:UTF-8'')?["']?([^";\n]+)/i.exec(dispo);
253 if (m?.[1]) {
254 try {
255 filename = decodeURIComponent(m[1].replace(/['"]/g, "").trim());
256 } catch {
257 filename = m[1].replace(/['"]/g, "").trim();
258 }
259 }
260 }
261 const objectUrl = URL.createObjectURL(blob);
262 const link = document.createElement("a");
263 link.href = objectUrl;
264 link.download = filename;
265 document.body.appendChild(link);
266 link.click();
267 document.body.removeChild(link);
268 URL.revokeObjectURL(objectUrl);
269 }
270
271 async function downloadPaymentInvoiceBinary(paymentId: number): Promise<void> {
272 const { base, nonce } = getAccountRestConfig();
273 if (!nonce) {
274 throw new Error(
275 __(
276 "Missing security token. Reload the account page and try again.",
277 "yatra",
278 ),
279 );
280 }
281 const url = buildPaymentInvoiceUrl(base, paymentId, "download");
282 const res = await fetch(url, {
283 method: "GET",
284 credentials: "include",
285 headers: {
286 "X-WP-Nonce": nonce,
287 Accept: "application/pdf, application/octet-stream, */*",
288 },
289 });
290 if (!res.ok) {
291 throw new Error(await readFetchErrorMessage(res));
292 }
293 const blob = await res.blob();
294 const dispo = res.headers.get("Content-Disposition");
295 let filename = `invoice-${paymentId}.pdf`;
296 if (dispo) {
297 const m = /filename\*?=(?:UTF-8'')?["']?([^";\n]+)/i.exec(dispo);
298 if (m?.[1]) {
299 try {
300 filename = decodeURIComponent(m[1].replace(/['"]/g, "").trim());
301 } catch {
302 filename = m[1].replace(/['"]/g, "").trim();
303 }
304 }
305 }
306 const objectUrl = URL.createObjectURL(blob);
307 const link = document.createElement("a");
308 link.href = objectUrl;
309 link.download = filename;
310 document.body.appendChild(link);
311 link.click();
312 document.body.removeChild(link);
313 URL.revokeObjectURL(objectUrl);
314 }
315
316 export interface DocumentDownloadOptions {
317 bookingId?: number;
318 paymentId?: number;
319 documentType: "voucher" | "invoice" | "itinerary" | "all" | "downloads";
320 fallbackUrl?: string;
321 }
322
323 export const downloadDocument = async (
324 options: DocumentDownloadOptions,
325 ): Promise<void> => {
326 if (options.documentType === "downloads" && options.fallbackUrl) {
327 window.open(options.fallbackUrl, "_blank", "noopener,noreferrer");
328 return;
329 }
330
331 if (options.documentType === "invoice") {
332 const pid =
333 options.paymentId ?? parsePaymentIdFromHref(options.fallbackUrl || "");
334 if (pid) {
335 await downloadPaymentInvoiceBinary(pid);
336 return;
337 }
338 }
339
340 if (
341 options.documentType === "voucher" ||
342 options.documentType === "itinerary"
343 ) {
344 const kind = options.documentType === "voucher" ? "voucher" : "itinerary";
345 const bid =
346 options.bookingId ??
347 parseBookingDocFromHref(options.fallbackUrl || "", kind);
348 if (bid) {
349 await downloadBookingBinary(bid, kind);
350 return;
351 }
352 }
353
354 if (options.bookingId && options.documentType === "all") {
355 await downloadBookingBinary(options.bookingId, "voucher");
356 return;
357 }
358
359 console.error(`No handler for document type: ${options.documentType}`);
360 };
361
362 export const downloadVoucher = (bookingId: number) =>
363 downloadBookingBinary(bookingId, "voucher");
364
365 export const downloadInvoice = (paymentId: number) =>
366 downloadPaymentInvoiceBinary(paymentId);
367
368 export const downloadItinerary = (bookingId: number) =>
369 downloadBookingBinary(bookingId, "itinerary");
370
371 /** Open payment invoice PDF in a new tab (REST preview JSON → blob). */
372 export const previewPaymentInvoice = async (
373 paymentId: number,
374 ): Promise<void> => {
375 const { base, nonce } = getAccountRestConfig();
376 if (!nonce) {
377 throw new Error(
378 __(
379 "Missing security token. Reload the account page and try again.",
380 "yatra",
381 ),
382 );
383 }
384 const url = buildPaymentInvoiceUrl(base, paymentId, "preview");
385 const blob = await fetchPreviewPdf(url);
386 openPdfInNewTab(blob);
387 };
388
389 export async function previewTravelDocument(doc: {
390 category: string;
391 url: string;
392 booking_id?: number;
393 payment_id?: number;
394 }): Promise<void> {
395 const { base, nonce } = getAccountRestConfig();
396 if (!nonce) {
397 throw new Error(
398 __(
399 "Missing security token. Reload the account page and try again.",
400 "yatra",
401 ),
402 );
403 }
404
405 if (doc.category === "invoice") {
406 const pid = doc.payment_id ?? parsePaymentIdFromHref(doc.url);
407 if (!pid) {
408 throw new Error(__("Could not resolve invoice link.", "yatra"));
409 }
410 const url = buildPaymentInvoiceUrl(base, pid, "preview");
411 const blob = await fetchPreviewPdf(url);
412 openPdfInNewTab(blob);
413 return;
414 }
415
416 if (doc.category === "voucher" || doc.category === "itinerary") {
417 const kind = doc.category === "voucher" ? "voucher" : "itinerary";
418 const bid = doc.booking_id ?? parseBookingDocFromHref(doc.url, kind);
419 if (!bid) {
420 throw new Error(__("Could not resolve document link.", "yatra"));
421 }
422 const url = buildBookingDocumentUrl(base, bid, kind, "preview");
423 const blob = await fetchPreviewPdf(url);
424 openPdfInNewTab(blob);
425 return;
426 }
427
428 window.open(doc.url, "_blank", "noopener,noreferrer");
429 }
430