Initial commit: banking statement download scripts
This commit is contained in:
@@ -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.
|
||||||
@@ -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.
|
||||||
@@ -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('');
|
||||||
|
})();
|
||||||
@@ -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 <p> 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('');
|
||||||
|
})();
|
||||||
@@ -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('');
|
||||||
|
})();
|
||||||
Reference in New Issue
Block a user