commit 68c5d561a61f5443069b1480bf9b326d1d475d3c Author: siddharthd Date: Thu Mar 12 11:51:16 2026 +1100 Initial commit: banking statement download scripts diff --git a/README.md b/README.md new file mode 100644 index 0000000..119f42e --- /dev/null +++ b/README.md @@ -0,0 +1,8 @@ +# scripts + +A collection of utility scripts. + +## banking/ + +Browser console scripts for downloading bank statements as named PDF files. +See [banking/README.md](banking/README.md) for usage. diff --git a/banking/README.md b/banking/README.md new file mode 100644 index 0000000..e06300c --- /dev/null +++ b/banking/README.md @@ -0,0 +1,53 @@ +# Bank Statement Download Helpers + +Browser console scripts to save bank statements as properly named PDF files. + +--- + +## ANZ — `anz-download-statements.js` + +### How to Use + +1. Log into ANZ Internet Banking → **View statements and notices** +2. Expand all year sections so all statements are visible +3. Open **F12 → Console** +4. Paste the contents of `anz-download-statements.js` and press **Enter** +5. You'll see: `✅ Download helper active!` +6. Click each **Download** button — the PDF saves with a name like `ANZ-Statement-2026-02-February.pdf` +7. Close the blob tab that opens, then click the next Download button + +### Output + +`ANZ-Statement-YYYY-MM-Month.pdf` (e.g., `ANZ-Statement-2026-02-February.pdf`) + +### How It Works + +Listens to ANZ's XHR API responses (read-only) and extracts the base64-encoded PDF data from the JSON response. Triggers a file download with a descriptive filename. ANZ's normal flow is unaffected — the blob tab still opens. + +--- + +## Westpac — `westpac-download-statements.js` + +### How to Use + +1. Log into Westpac Online Banking → **Search for statements** +2. Select the account and date range, then click **Search** +3. Click **Display more** if needed to show all statements +4. Open **F12 → Console** +5. Paste the contents of `westpac-download-statements.js` and press **Enter** +6. You'll see: `✅ Westpac download helper active!` +7. Click each **Download PDF** button — the PDF saves with a proper name instead of opening in a new tab + +### Output + +`Westpac-AccountName-YYYY-MM-Month.pdf` (e.g., `Westpac-Altitude-Qantas-Black-Card-2026-02-February.pdf`) + +### How It Works + +Intercepts clicks on the Download PDF links, fetches the PDF using the existing authenticated session, and saves it as a named file download. Prevents the default behaviour of opening the PDF in a new tab. + +--- + +## To Stop + +Refresh the page (F5) for either script. diff --git a/banking/anz-download-statements.js b/banking/anz-download-statements.js new file mode 100644 index 0000000..78687a9 --- /dev/null +++ b/banking/anz-download-statements.js @@ -0,0 +1,101 @@ +// ============================================================================= +// ANZ Statement Download Helper +// ============================================================================= +// PASTE + ENTER, then click Download buttons normally. +// The blob tab still opens (just close it), BUT you also get a properly +// named PDF file downloaded automatically. +// +// TO STOP: just refresh the page (F5) +// ============================================================================= + +(function () { + 'use strict'; + + let downloadCount = 0; + let lastClickedFilename = null; + + // Track which button was clicked + document.addEventListener('click', function (e) { + const btn = e.target.closest('button[data-clicktrack="Download"]'); + if (btn) { + const label = btn.getAttribute('aria-label') || ''; + const match = label.match( + /Download the statement issued on (\d{2}) (\w{3}) (\d{4}) (\w+) \d{4}/ + ); + if (match) { + const [, , monthShort, year, monthFull] = match; + const monthNum = new Date(`${monthShort} 1, 2000`).getMonth() + 1; + lastClickedFilename = `ANZ-Statement-${year}-${String(monthNum).padStart(2, '0')}-${monthFull}.pdf`; + } else { + lastClickedFilename = `ANZ-Statement-${Date.now()}.pdf`; + } + console.log(`⏳ ${lastClickedFilename}...`); + } + }, true); + + // Intercept XHR — READ ONLY, don't modify anything + const origOpen = XMLHttpRequest.prototype.open; + const origSend = XMLHttpRequest.prototype.send; + + XMLHttpRequest.prototype.open = function (method, url, ...rest) { + this._url = url; + return origOpen.call(this, method, url, ...rest); + }; + + XMLHttpRequest.prototype.send = function (body) { + if (this._url && this._url.includes('download-statement')) { + // Just listen — don't modify anything. ANZ's code runs normally. + this.addEventListener('load', function () { + try { + const text = typeof this.response === 'string' ? this.response : this.responseText; + const json = JSON.parse(text); + const pdf = findPdfData(json); + if (pdf) { + const fn = lastClickedFilename || `ANZ-Statement-${Date.now()}.pdf`; + savePdf(pdf, fn); + downloadCount++; + console.log(`✅ #${downloadCount} ${fn} (${(pdf.length * 3 / 4 / 1024).toFixed(0)} KB)`); + console.log(` Close the blob tab, then click next Download button.`); + } + } catch (e) { + // Silently ignore — ANZ's handler still runs fine + } + }); + } + return origSend.call(this, body); + }; + + function findPdfData(obj, depth) { + if ((depth || 0) > 5) return null; + if (typeof obj === 'string' && obj.length > 1000) { + if (obj.startsWith('JVBERi')) return obj; + if (obj.match(/^[A-Za-z0-9+/=]+$/) && obj.length > 10000) return obj; + } + if (Array.isArray(obj)) { + for (const item of obj) { const r = findPdfData(item, (depth || 0) + 1); if (r) return r; } + } + if (obj && typeof obj === 'object') { + for (const key of Object.keys(obj)) { const r = findPdfData(obj[key], (depth || 0) + 1); if (r) return r; } + } + return null; + } + + function savePdf(base64, filename) { + const bin = atob(base64); + const bytes = new Uint8Array(bin.length); + for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i); + const blob = new Blob([bytes], { type: 'application/pdf' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; a.download = filename; + document.body.appendChild(a); a.click(); document.body.removeChild(a); + setTimeout(() => URL.revokeObjectURL(url), 2000); + } + + console.log(''); + console.log('✅ Download helper active!'); + console.log('👆 Click Download buttons normally.'); + console.log(' Each click: PDF saves as file + blob tab opens (close it).'); + console.log(' To stop: refresh (F5)'); + console.log(''); +})(); diff --git a/banking/mycard-download-statements.js b/banking/mycard-download-statements.js new file mode 100644 index 0000000..8dd4a93 --- /dev/null +++ b/banking/mycard-download-statements.js @@ -0,0 +1,142 @@ +// ============================================================================= +// MyCard Statement Download Helper (mycard.com.au) +// ============================================================================= +// PASTE + ENTER, then click statement rows normally. +// The blob tab still opens (just close it), BUT you also get a properly +// named PDF saved automatically. +// +// TO STOP: just refresh the page (F5) +// ============================================================================= + +(function () { + 'use strict'; + + if (window._mcStatementHelper) { + if (window._mcOrigWindowOpen) window.open = window._mcOrigWindowOpen; + console.log('🔄 Restarting MyCard helper...'); + } + window._mcStatementHelper = true; + + let downloadCount = 0; + let lastClickedDate = null; + const downloadedSet = new Set(); + + const monthNames = ['', 'January', 'February', 'March', 'April', 'May', 'June', + 'July', 'August', 'September', 'October', 'November', 'December']; + + function extractDate(text) { + const m = text.match(/(\d{1,2})\s+(January|February|March|April|May|June|July|August|September|October|November|December)\s+(\d{4})/); + if (!m) return null; + const monthNum = new Date(`${m[2]} 1, 2000`).getMonth() + 1; + return { year: m[3], monthNum: String(monthNum).padStart(2, '0'), monthName: m[2] }; + } + + // Click events compose through shadow DOM — use composedPath() to read inner elements + document.addEventListener('click', function (e) { + const path = e.composedPath(); + for (const el of path) { + if (!(el instanceof HTMLElement)) continue; + // Search this element and its children for

with a date + const paragraphs = el.querySelectorAll ? el.querySelectorAll('p') : []; + for (const p of paragraphs) { + const date = extractDate(p.textContent.trim()); + if (date) { + lastClickedDate = date; + console.log(`⏳ MyCard-Statement-${date.year}-${date.monthNum}-${date.monthName}.pdf...`); + return; + } + } + } + }, true); + + // Also try mousedown with composedPath + document.addEventListener('mousedown', function (e) { + const path = e.composedPath(); + for (const el of path) { + if (!(el instanceof HTMLElement)) continue; + const paragraphs = el.querySelectorAll ? el.querySelectorAll('p') : []; + for (const p of paragraphs) { + const date = extractDate(p.textContent.trim()); + if (date) { + lastClickedDate = date; + return; + } + } + } + }, true); + + // Search all shadow roots for dates (fallback) + function findDateInShadowDom() { + function searchNode(node) { + if (node.shadowRoot) { + const ps = node.shadowRoot.querySelectorAll('p'); + for (const p of ps) { + const date = extractDate(p.textContent.trim()); + if (date) return date; + } + for (const child of node.shadowRoot.querySelectorAll('*')) { + const result = searchNode(child); + if (result) return result; + } + } + return null; + } + for (const el of document.querySelectorAll('*')) { + const result = searchNode(el); + if (result) return result; + } + return null; + } + + function savePdf(blob, filename) { + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; a.download = filename; + document.body.appendChild(a); a.click(); document.body.removeChild(a); + setTimeout(() => URL.revokeObjectURL(url), 2000); + } + + // Intercept window.open — fetch blob URL and save as named PDF + const origWindowOpen = window._mcOrigWindowOpen || window.open; + window._mcOrigWindowOpen = origWindowOpen; + window.open = function (url, ...rest) { + const win = origWindowOpen.call(this, url, ...rest); + + if (typeof url === 'string' && url.startsWith('blob:')) { + if (downloadedSet.has(url)) return win; + downloadedSet.add(url); + setTimeout(() => downloadedSet.delete(url), 5000); + + // Try click-tracked date, then shadow DOM scan + const date = lastClickedDate || findDateInShadowDom(); + + fetch(url) + .then(r => r.blob()) + .then(blob => { + return blob.slice(0, 5).text().then(h => { + if (h !== '%PDF-') return; + let fn; + if (date) { + fn = `MyCard-Statement-${date.year}-${date.monthNum}-${date.monthName}.pdf`; + } else { + fn = `MyCard-Statement-${Date.now()}.pdf`; + } + savePdf(blob, fn); + downloadCount++; + console.log(`✅ #${downloadCount} ${fn} (${(blob.size / 1024).toFixed(0)} KB)`); + console.log(` Close the blob tab, then click next statement.`); + }); + }) + .catch(err => console.error('❌ Failed:', err)); + } + + return win; + }; + + console.log(''); + console.log('✅ MyCard download helper active!'); + console.log('👆 Click statement rows normally.'); + console.log(' Each click: PDF saves as file + blob tab opens (close it).'); + console.log(' To stop: refresh (F5)'); + console.log(''); +})(); diff --git a/banking/westpac-download-statements.js b/banking/westpac-download-statements.js new file mode 100644 index 0000000..f103309 --- /dev/null +++ b/banking/westpac-download-statements.js @@ -0,0 +1,106 @@ +// ============================================================================= +// Westpac Statement Download Helper +// ============================================================================= +// PASTE + ENTER, then click Download PDF buttons normally. +// Instead of opening in a new tab, the PDF saves as a properly named file. +// +// TO STOP: just refresh the page (F5) +// ============================================================================= + +(function () { + 'use strict'; + + let downloadCount = 0; + + // Get the currently selected account name for the filename + function getAccountName() { + const select = document.getElementById('AccountGlobalId'); + if (!select) return 'Account'; + const text = select.options[select.selectedIndex].text.trim(); + // Extract just the account name (before the number/balance) + // e.g., "Altitude Qantas Black Card xxxx xxxx xx77 8032 -$10,272.88" + // → "Altitude-Qantas-Black-Card" + // e.g., "Westpac Choice 733-501 714878 $0.00" + // → "Westpac-Choice" + const match = text.match(/^(.+?)\s{2,}/); + if (match) { + return match[1].trim().replace(/\s+/g, '-'); + } + return text.split(/\s{2,}/)[0].trim().replace(/\s+/g, '-'); + } + + // Parse date from the statement row + function getDateFromRow(row) { + const dateCell = row.querySelector('td.date span[data-bind*="FormattedDate"]'); + if (!dateCell) return null; + const text = dateCell.textContent.trim(); // e.g., "22 Feb 2026" + const match = text.match(/(\d{1,2})\s+(\w{3})\s+(\d{4})/); + if (!match) return null; + const [, day, monthShort, year] = match; + const monthNum = new Date(`${monthShort} 1, 2000`).getMonth() + 1; + const monthNames = ['', 'January', 'February', 'March', 'April', 'May', 'June', + 'July', 'August', 'September', 'October', 'November', 'December']; + return { + year, + monthNum: String(monthNum).padStart(2, '0'), + monthName: monthNames[monthNum] + }; + } + + // Intercept clicks on Download PDF links + document.addEventListener('click', function (e) { + const link = e.target.closest('td.tf-actions a.btn'); + if (!link) return; + + const href = link.getAttribute('href'); + if (!href || !href.includes('getpdfstatement')) return; + + // Prevent the default (opening in new tab) + e.preventDefault(); + e.stopPropagation(); + + const row = link.closest('tr'); + const dateInfo = row ? getDateFromRow(row) : null; + const accountName = getAccountName(); + + let filename; + if (dateInfo) { + filename = `Westpac-${accountName}-${dateInfo.year}-${dateInfo.monthNum}-${dateInfo.monthName}.pdf`; + } else { + filename = `Westpac-${accountName}-${Date.now()}.pdf`; + } + + console.log(`⏳ Downloading ${filename}...`); + + // Fetch the PDF and save it + fetch(href, { credentials: 'include' }) + .then(response => { + if (!response.ok) throw new Error(`HTTP ${response.status}`); + return response.blob(); + }) + .then(blob => { + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = filename; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + setTimeout(() => URL.revokeObjectURL(url), 2000); + downloadCount++; + console.log(`✅ #${downloadCount} ${filename} (${(blob.size / 1024).toFixed(0)} KB)`); + }) + .catch(err => { + console.error(`❌ Failed to download ${filename}:`, err); + console.log(' Opening in new tab as fallback...'); + window.open(href, '_blank'); + }); + }, true); + + console.log(''); + console.log('✅ Westpac download helper active!'); + console.log('👆 Click Download PDF buttons normally.'); + console.log(' Each click: PDF saves as a named file (no new tab).'); + console.log(' To stop: refresh (F5)'); + console.log(''); +})();