PageSourceSearch

https://www.westpac.com.au/scripts/disclaimer-query-utils.js

js westpac.com.au collected 2026-09-24 08:29:45 UTC 5,824 bytes, 172 lines download raw bytes

1import { DEFAULT_DISCLAIMER_ROOT, DISCLAIMER_ENDPOINT } from './disclaimer-config.js';
2
3const requestCache = new Map();
4
5function createDisclaimerResult(overrides = {}) {
6  return {
7    top: [],
8    disclaimers: [],
9    bottom: [],
10    warnings: [],
11    requestSucceeded: false,
12    ...overrides,
13  };
14}
15
16export function normalizeDisclaimerPath(path, disclaimerRoot = DEFAULT_DISCLAIMER_ROOT) {
17  if (typeof path !== 'string') return null;
18
19  let normalizedPath = path;
20  while (normalizedPath.endsWith('/')) {
21    normalizedPath = normalizedPath.slice(0, -1);
22  }
23  if (normalizedPath.endsWith('.html')) {
24    normalizedPath = normalizedPath.slice(0, -'.html'.length);
25  }
26
27  const rootPrefix = `${disclaimerRoot}/`;
28  if (!normalizedPath.startsWith(rootPrefix)) return null;
29
30  const relativePath = normalizedPath.slice(rootPrefix.length);
31  if (!relativePath || relativePath.includes('%')) return null;
32
33  const segments = relativePath.split('/');
34  const hasValidSegments = segments.every((segment) => {
35    const isRelativeSegment = segment !== '.' && segment !== '..';
36    const hasSafeCharacters = /^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(segment);
37    return isRelativeSegment && hasSafeCharacters;
38  });
39
40  return hasValidSegments ? relativePath : null;
41}
42
43function normalizeContentFragmentEntries(entries, disclaimerRoot, requiresNumber = false) {
44  if (!Array.isArray(entries)) return null;
45
46  const normalizedPaths = new Set();
47  const hasValidEntries = entries.every((entry, index) => {
48    const relativePath = normalizeDisclaimerPath(entry?.path, disclaimerRoot);
49    const hasValidPath = relativePath !== null;
50    const hasUniquePath = !normalizedPaths.has(relativePath);
51    const hasValidHtml = typeof entry.html === 'string';
52    const hasValidNumber = !requiresNumber || entry.number === index + 1;
53    if (![hasValidPath, hasUniquePath, hasValidHtml, hasValidNumber].every(Boolean)) return false;
54
55    normalizedPaths.add(relativePath);
56    return true;
57  });
58  if (!hasValidEntries) return null;
59
60  return entries.map((entry) => ({
61    ...(requiresNumber ? { number: entry.number } : {}),
62    path: `${disclaimerRoot}/${normalizeDisclaimerPath(entry.path, disclaimerRoot)}`,
63    html: entry.html,
64  }));
65}
66
67function hasValidWarnings(warnings) {
68  if (warnings === undefined) return true;
69  if (!Array.isArray(warnings)) return false;
70
71  return warnings.every((warning) => {
72    const hasType = typeof warning?.type === 'string';
73    const hasMessage = typeof warning.message === 'string';
74    const hasPath = typeof warning.path === 'string';
75    return hasType && hasMessage && hasPath;
76  });
77}
78
79function normalizeDisclaimerResponse(data, disclaimerRoot) {
80  if (!data || !hasValidWarnings(data.warnings)) return null;
81
82  const top = normalizeContentFragmentEntries(data.top, disclaimerRoot);
83  const disclaimers = normalizeContentFragmentEntries(data.disclaimers, disclaimerRoot, true);
84  const bottom = normalizeContentFragmentEntries(data.bottom, disclaimerRoot);
85  if (!top || !disclaimers || !bottom) return null;
86
87  return {
88    top,
89    disclaimers,
90    bottom,
91    warnings: data.warnings || [],
92  };
93}
94
95export function collectDisclaimerQueryEntries(main, blockData = {}, options = {}) {
96  const { disclaimerRoot = DEFAULT_DISCLAIMER_ROOT } = options;
97  const entries = [];
98  const topPath = normalizeDisclaimerPath(blockData.topCFPath, disclaimerRoot);
99  if (topPath) entries.push(`top~${topPath}`);
100
101  const uniqueFootnotePaths = new Set();
102  const links = main.querySelectorAll(`a[href^="${disclaimerRoot}/"]`);
103  links.forEach((link) => {
104    if (link.closest('.disclaimer')) return;
105    if (link.textContent.trim() !== '[#]') return;
106
107    const relativePath = normalizeDisclaimerPath(link.getAttribute('href'), disclaimerRoot);
108    if (relativePath) uniqueFootnotePaths.add(relativePath);
109  });
110  entries.push(...uniqueFootnotePaths);
111
112  const bottomPath = normalizeDisclaimerPath(blockData.bottomCFPath, disclaimerRoot);
113  if (bottomPath) entries.push(`bottom~${bottomPath}`);
114
115  return entries;
116}
117
118export function buildDisclaimerQueryUrl(host, entries) {
119  const url = new URL(DISCLAIMER_ENDPOINT, host);
120  url.search = new URLSearchParams({ cf: entries.join(':') }).toString();
121  return url.toString();
122}
123
124function getCachedRequest(url, fetchImplementation) {
125  return requestCache.get(url)?.get(fetchImplementation);
126}
127
128function cacheRequest(url, fetchImplementation, request) {
129  const requestsByFetch = requestCache.get(url) || new Map();
130  requestsByFetch.set(fetchImplementation, request);
131  requestCache.set(url, requestsByFetch);
132}
133
134export async function fetchQueryDisclaimers(entries, options = {}) {
135  if (!Array.isArray(entries) || entries.length === 0) {
136    return createDisclaimerResult({ requestSucceeded: true });
137  }
138
139  const { host, disclaimerRoot = DEFAULT_DISCLAIMER_ROOT, fetchImplementation = fetch } = options;
140  if (!host || typeof fetchImplementation !== 'function') return createDisclaimerResult();
141
142  let url;
143  try {
144    url = buildDisclaimerQueryUrl(host, entries);
145  } catch {
146    return createDisclaimerResult();
147  }
148
149  const cacheKey = `${disclaimerRoot}:${url}`;
150  const cachedRequest = getCachedRequest(cacheKey, fetchImplementation);
151  if (cachedRequest) return cachedRequest;
152
153  const request = (async () => {
154    try {
155      const response = await fetchImplementation(url);
156      if (!response.ok) return createDisclaimerResult();
157
158      const normalizedResponse = normalizeDisclaimerResponse(await response.json(), disclaimerRoot);
159      if (!normalizedResponse) return createDisclaimerResult();
160
161      return createDisclaimerResult({
162        ...normalizedResponse,
163        requestSucceeded: true,
164      });
165    } catch {
166      return createDisclaimerResult();
167    }
168  })();
169
170  cacheRequest(cacheKey, fetchImplementation, request);
171  return request;
172}

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.