From b38ee62ef99af5e6ccb4131343e4e0d7c80617b0 Mon Sep 17 00:00:00 2001 From: Dias Baskara <25913324+diasbaskara@users.noreply.github.com> Date: Wed, 26 Nov 2025 10:47:44 +0700 Subject: [PATCH] Prevent sidebar injection in iframes and duplicate loads Added guards to ensure the userscript sidebar is only injected into the top window and not into iframes. Also prevents duplicate sidebar injection in the top window. Updated z-index for sidebar, improved DOM creation safety, and made minor UI text changes. --- coretabs.user.js | 1164 +++++++++++++++++++++++++++++++++++----------- 1 file changed, 889 insertions(+), 275 deletions(-) diff --git a/coretabs.user.js b/coretabs.user.js index bd9b6b0..dec19c9 100644 --- a/coretabs.user.js +++ b/coretabs.user.js @@ -10,25 +10,53 @@ // @run-at document-idle // ==/UserScript== -(function() { - 'use strict'; +(function () { + "use strict"; - // --- SCRIPT CONFIGURATION --- - const AUTH_STORAGE_KEY = 'cats-angular-clientuser:https://coretax.intranet.pajak.go.id/identityprovider:cats-angular-client'; - const DEFAULT_CASES_FILTER = 'In Progress'; - const REFUND_CASE_PREFIX = 'Pengembalian'; - // ---------------------------- + // Prevent running inside iframes and prevent duplicate injection into the top window. + // This ensures only one instance of the sidebar is created even if the userscript is executed + // for multiple frames on the same page. + try { + if (window.top !== window.self) { + // We're in an iframe - do nothing. + return; + } + } catch (e) { + // In rare cross-origin cases accessing window.top may throw; if so, abort to be safe. + return; + } - // --- State Management --- - let allMyCases = [], allCaseDocuments = [], allCaseUsers = [], refundReviewData = []; - let filteredRefundData = []; - let selectedCaseId = null; - let loadedDocsForCaseId = null; - let loadedUsersForCaseId = null; + // Guard against double-injection in the top window (e.g., userscript re-run) + if (window.__coretabs_injected) { + return; + } + Object.defineProperty(window, "__coretabs_injected", { + value: true, + configurable: false, + writable: false, + enumerable: false, + }); - function addStyles() { - GM_addStyle(` - #ct-sidebar{position:fixed;top:100px;right:-950px;width:950px;height:80vh;max-height:800px;background-color:#f9f9f9;border:1px solid #ccc;border-radius:8px 0 0 8px;box-shadow:-3px 0 8px rgba(0,0,0,.15);z-index:9999;transition:right .4s ease-in-out;display:flex;flex-direction:column;font-family:sans-serif}#ct-sidebar.open{right:0}#ct-sidebar-toggle{position:absolute;top:50%;right:950px;transform:translateY(-50%);width:30px;height:80px;background-color:#0056b3;color:#fff;border:none;border-radius:8px 0 0 8px;cursor:pointer;writing-mode:vertical-rl;text-orientation:mixed;display:flex;align-items:center;justify-content:center;font-size:14px;font-weight:700;letter-spacing:1px} + // --- SCRIPT CONFIGURATION --- + const AUTH_STORAGE_KEY = + "cats-angular-clientuser:https://coretax.intranet.pajak.go.id/identityprovider:cats-angular-client"; + const DEFAULT_CASES_FILTER = "In Progress"; + const REFUND_CASE_PREFIX = "Pengembalian"; + // ---------------------------- + + // --- State Management --- + let allMyCases = [], + allCaseDocuments = [], + allCaseUsers = [], + refundReviewData = []; + let filteredRefundData = []; + let selectedCaseId = null; + let loadedDocsForCaseId = null; + let loadedUsersForCaseId = null; + + function addStyles() { + GM_addStyle(` + #ct-sidebar{position:fixed;top:100px;right:-950px;width:950px;height:80vh;max-height:800px;background-color:#f9f9f9;border:1px solid #ccc;border-radius:8px 0 0 8px;box-shadow:-3px 0 8px rgba(0,0,0,.15);z-index:2147483647;transition:right .4s ease-in-out;display:flex;flex-direction:column;font-family:sans-serif}#ct-sidebar.open{right:0}#ct-sidebar-toggle{position:absolute;top:50%;right:950px;transform:translateY(-50%);width:30px;height:80px;background-color:#0056b3;color:#fff;border:none;border-radius:8px 0 0 8px;cursor:pointer;writing-mode:vertical-rl;text-orientation:mixed;display:flex;align-items:center;justify-content:center;font-size:14px;font-weight:700;letter-spacing:1px} #ct-header-area { padding: 12px 15px; background-color: #343a40; color: white; flex-shrink: 0; display: flex; align-items: center; gap: 15px; border-bottom: 1px solid #495057; } #ct-header-icon { flex-shrink: 0; } #ct-header-icon svg { width: 32px; height: 32px; fill: #e9ecef; } @@ -56,13 +84,16 @@ .currency-wrapper { display: flex; justify-content: space-between; } .currency-num { font-variant-numeric: tabular-nums; } `); - } + } - function createSidebar() { - const sidebarContainer = document.createElement('div'); - sidebarContainer.innerHTML = ` + function createSidebar() { + // ensure we don't create duplicate DOM elements even if something else tries to run + if (document.getElementById("ct-sidebar")) return; + + const sidebarContainer = document.createElement("div"); + sidebarContainer.innerHTML = `
`) + : (errorHtml = `An error occurred:
${error.message}
Step 1/3: Fetching Sub Process ID...
`; + if (!caseId) throw new Error("A case must be selected."); + const subProcessId = await fetchSubProcessId(caseId); + responseArea.innerHTML = `Step 2/3: Fetching reference number (trying primary API)...
`; + const referenceNumber = await fetchC02FormDetail(caseId, subProcessId); + responseArea.innerHTML = `Step 3/3: Fetching refund details...
`; + await fetchRefundReview(caseId, referenceNumber); + } catch (error) { + handleError(error, responseArea); + } finally { + button.textContent = originalText; + button.disabled = false; + } + } + + function handleCaseSelection(event) { + const selectedRow = event.target.closest("tr"); + if (!selectedRow || selectedRow.classList.contains("group-header")) return; + const caseId = selectedRow.dataset.id; + if (!caseId || caseId === selectedCaseId) return; + + const selectedCase = allMyCases.find( + (e) => e.AggregateIdentifier === caseId, + ); + updateHeader(selectedCase); + selectedCaseId = caseId; + loadedDocsForCaseId = null; + loadedUsersForCaseId = null; + refundReviewData = []; + filteredRefundData = []; + document.querySelector("#tab-refund .results-container").innerHTML = + `Please use the 'Review Refund' action on a relevant case.
`; + document.getElementById("refund-download-btn").disabled = true; + + const allRows = selectedRow.closest("tbody").querySelectorAll("tr"); + allRows.forEach((e) => e.classList.remove("selected")); + selectedRow.classList.add("selected"); + + const actionButton = event.target.closest(".action-btn"); + if (actionButton) { + if (actionButton.matches(".view-docs")) { + switchTab("tab-docs"); + } else if (actionButton.matches(".view-users")) { + switchTab("tab-users"); + } else if (actionButton.matches(".review-refund-case")) { + startRefundReviewProcess(caseId, actionButton); + } + } + } + function handleDocumentAction(event) { + const target = event.target.closest(".action-btn"); + if (!target) return; + if (target.matches(".download-doc")) { + const docId = target.dataset.docId; + const filename = target.dataset.filename; + downloadDocument(docId, filename, target); + } + } + function handleGroupToggle(event) { + const headerRow = event.target.closest(".group-header"); + if (!headerRow) return; + const groupId = headerRow.dataset.groupId; + if (!groupId) return; + headerRow.classList.toggle("collapsed"); + headerRow.classList.toggle("expanded"); + const isCollapsed = headerRow.classList.contains("collapsed"); + const tableBody = headerRow.parentElement; + const memberRows = tableBody.querySelectorAll(`.group-member.${groupId}`); + memberRows.forEach((row) => { + row.style.display = isCollapsed ? "none" : "table-row"; + }); + } + function switchTab(tabId) { + (document + .querySelectorAll(".ct-tab-button") + .forEach((e) => e.classList.remove("active")), + document + .querySelectorAll(".ct-tab-panel") + .forEach((e) => e.classList.remove("active")), + document.querySelector(`[data-tab="${tabId}"]`).classList.add("active"), + document.getElementById(tabId).classList.add("active"), + "tab-docs" === tabId + ? selectedCaseId && + selectedCaseId !== loadedDocsForCaseId && + fetchCaseDocuments(selectedCaseId) + : "tab-users" === tabId && + selectedCaseId && + selectedCaseId !== loadedUsersForCaseId && + fetchCaseUsers(selectedCaseId)); + } + function downloadRefundExcel() { + if (!filteredRefundData || filteredRefundData.length === 0) { + alert("No data to download. Please filter the data first."); + return; + } + const now = new Date(); + const year = now.getFullYear(); + const month = String(now.getMonth() + 1).padStart(2, "0"); + const day = String(now.getDate()).padStart(2, "0"); + const hours = String(now.getHours()).padStart(2, "0"); + const minutes = String(now.getMinutes()).padStart(2, "0"); + const datetimeSuffix = `${year}${month}${day}_${hours}${minutes}`; + let filenamePrefix = "Refund_Review_Data"; + if (selectedCaseId) { + const selectedCase = allMyCases.find( + (c) => c.AggregateIdentifier === selectedCaseId, + ); + if (selectedCase && selectedCase.CaseNumber) { + const sanitizedCaseNumber = selectedCase.CaseNumber.replace( + /[\\/:"*?<>|]/g, + "_", + ); + filenamePrefix = `Refund_Review_Data_${sanitizedCaseNumber}`; + } + } + const filename = `${filenamePrefix}_${datetimeSuffix}.xlsx`; + const headerMapping = [ + { + apiField: "ReportedBySeller", + excelHeader: "Telah dilaporkan oleh Penjual?", + }, + { apiField: "Tin", excelHeader: "NPWP Penjual" }, + { apiField: "Name", excelHeader: "Nama Penjual" }, + { + apiField: "DocumentNumber", + excelHeader: + "Nomor Faktur Pajak/Dokumen yang Dipersamakan/Nota Retur/Nota Pembatalan", + }, + { + apiField: "DocumentDate", + excelHeader: + "Tanggal Faktur Pajak/Dokumen yang Dipersamakan/Nota Retur/Nota Pembatalan", + }, + { apiField: "TransactionCode", excelHeader: "Kode Transaksi" }, + { + apiField: "SellingPrice", + excelHeader: + "Harga Jual/Dasar Pengenaan Pajak/Dasar Pengenaan Pajak Lainnya (Rp)", + }, + { + apiField: "VatPaid", + excelHeader: "PPN yang dikreditkan pada SPT yang Dilaporkan", + }, + { + apiField: "StlgPaid", + excelHeader: "PPnBM yang dikreditkan pada SPT yang Dilaporkan", + }, + ]; + const excelData = filteredRefundData.map((item) => { + const row = {}; + headerMapping.forEach((map) => { + if (map.apiField === "ReportedBySeller") { + row[map.excelHeader] = item[map.apiField] ? "Yes" : "No"; } else { - titleEl.textContent = 'No Case Selected'; - subtitleEl.textContent = 'Please select a case from the "My Cases" tab'; - } - } - - // MODIFIED: This function has updated user-facing messages. - async function startRefundReviewProcess(caseId, button) { - const originalText = button.textContent; - button.textContent = '...'; - button.disabled = true; - const responseArea = document.querySelector('#tab-refund .results-container'); - try { - switchTab('tab-refund'); - responseArea.innerHTML = `Step 1/3: Fetching Sub Process ID...
`; - if (!caseId) throw new Error("A case must be selected."); - const subProcessId = await fetchSubProcessId(caseId); - responseArea.innerHTML = `Step 2/3: Fetching reference number (trying primary API)...
`; - const referenceNumber = await fetchC02FormDetail(caseId, subProcessId); - responseArea.innerHTML = `Step 3/3: Fetching refund details...
`; - await fetchRefundReview(caseId, referenceNumber); - } catch(error) { - handleError(error, responseArea); - } finally { - button.textContent = originalText; - button.disabled = false; + row[map.excelHeader] = + item[map.apiField] === null ? "" : item[map.apiField]; } + }); + return row; + }); + const worksheet = XLSX.utils.json_to_sheet(excelData); + const workbook = XLSX.utils.book_new(); + XLSX.utils.book_append_sheet(workbook, worksheet, "Refund Review"); + XLSX.writeFile(workbook, filename); + } + + function toggleAllGroups(containerSelector, button) { + const isCollapsing = button.textContent === "Collapse All"; + const headers = document.querySelectorAll( + `${containerSelector} .group-header`, + ); + const members = document.querySelectorAll( + `${containerSelector} .group-member`, + ); + + headers.forEach((h) => { + h.classList.toggle("expanded", !isCollapsing); + h.classList.toggle("collapsed", isCollapsing); + }); + members.forEach((m) => { + m.style.display = isCollapsing ? "none" : "table-row"; + }); + + button.textContent = isCollapsing ? "Expand All" : "Collapse All"; + } + + // --- MAIN INITIALIZATION --- + function main() { + addStyles(); + createSidebar(); + const toggleBtn = document.getElementById("ct-sidebar-toggle"); + if (toggleBtn) { + toggleBtn.addEventListener("click", () => { + const sidebar = document.getElementById("ct-sidebar"); + if (sidebar) sidebar.classList.toggle("open"); + }); } - function handleCaseSelection(event) { - const selectedRow = event.target.closest("tr"); - if (!selectedRow || selectedRow.classList.contains("group-header")) return; - const caseId = selectedRow.dataset.id; - if (!caseId || caseId === selectedCaseId) return; - - const selectedCase = allMyCases.find(e => e.AggregateIdentifier === caseId); - updateHeader(selectedCase); - selectedCaseId = caseId; - loadedDocsForCaseId = null; - loadedUsersForCaseId = null; - refundReviewData = []; - filteredRefundData = []; - document.querySelector('#tab-refund .results-container').innerHTML = `Please use the 'Review Refund' action on a relevant case.
`; - document.getElementById('refund-download-btn').disabled = true; - - const allRows = selectedRow.closest("tbody").querySelectorAll("tr"); - allRows.forEach(e => e.classList.remove("selected")); - selectedRow.classList.add("selected"); - - const actionButton = event.target.closest(".action-btn"); - if (actionButton) { - if (actionButton.matches(".view-docs")) { switchTab("tab-docs"); } - else if (actionButton.matches(".view-users")) { switchTab("tab-users"); } - else if (actionButton.matches(".review-refund-case")) { startRefundReviewProcess(caseId, actionButton); } - } - } - function handleDocumentAction(event) { - const target = event.target.closest('.action-btn'); - if (!target) return; - if (target.matches('.download-doc')) { - const docId = target.dataset.docId; - const filename = target.dataset.filename; - downloadDocument(docId, filename, target); - } - } - function handleGroupToggle(event) { - const headerRow = event.target.closest('.group-header'); - if (!headerRow) return; - const groupId = headerRow.dataset.groupId; - if (!groupId) return; - headerRow.classList.toggle('collapsed'); - headerRow.classList.toggle('expanded'); - const isCollapsed = headerRow.classList.contains('collapsed'); - const tableBody = headerRow.parentElement; - const memberRows = tableBody.querySelectorAll(`.group-member.${groupId}`); - memberRows.forEach(row => { row.style.display = isCollapsed ? 'none' : 'table-row'; }); - } - function switchTab(tabId){document.querySelectorAll(".ct-tab-button").forEach(e=>e.classList.remove("active")),document.querySelectorAll(".ct-tab-panel").forEach(e=>e.classList.remove("active")),document.querySelector(`[data-tab="${tabId}"]`).classList.add("active"),document.getElementById(tabId).classList.add("active"),"tab-docs"===tabId?selectedCaseId&&selectedCaseId!==loadedDocsForCaseId&&fetchCaseDocuments(selectedCaseId):"tab-users"===tabId&&selectedCaseId&&selectedCaseId!==loadedUsersForCaseId&&fetchCaseUsers(selectedCaseId)} - function downloadRefundExcel() { - if (!filteredRefundData || filteredRefundData.length === 0) { - alert("No data to download. Please filter the data first."); - return; - } - const now = new Date(); - const year = now.getFullYear(); - const month = String(now.getMonth() + 1).padStart(2, '0'); - const day = String(now.getDate()).padStart(2, '0'); - const hours = String(now.getHours()).padStart(2, '0'); - const minutes = String(now.getMinutes()).padStart(2, '0'); - const datetimeSuffix = `${year}${month}${day}_${hours}${minutes}`; - let filenamePrefix = 'Refund_Review_Data'; - if (selectedCaseId) { - const selectedCase = allMyCases.find(c => c.AggregateIdentifier === selectedCaseId); - if (selectedCase && selectedCase.CaseNumber) { - const sanitizedCaseNumber = selectedCase.CaseNumber.replace(/[\\/:"*?<>|]/g, '_'); - filenamePrefix = `Refund_Review_Data_${sanitizedCaseNumber}`; - } - } - const filename = `${filenamePrefix}_${datetimeSuffix}.xlsx`; - const headerMapping = [ - { apiField: "ReportedBySeller", excelHeader: "Telah dilaporkan oleh Penjual?" }, { apiField: "Tin", excelHeader: "NPWP Penjual" }, { apiField: "Name", excelHeader: "Nama Penjual" }, { apiField: "DocumentNumber", excelHeader: "Nomor Faktur Pajak/Dokumen yang Dipersamakan/Nota Retur/Nota Pembatalan" }, { apiField: "DocumentDate", excelHeader: "Tanggal Faktur Pajak/Dokumen yang Dipersamakan/Nota Retur/Nota Pembatalan" }, { apiField: "TransactionCode", excelHeader: "Kode Transaksi" }, { apiField: "SellingPrice", excelHeader: "Harga Jual/Dasar Pengenaan Pajak/Dasar Pengenaan Pajak Lainnya (Rp)" }, { apiField: "VatPaid", excelHeader: "PPN yang dikreditkan pada SPT yang Dilaporkan" }, { apiField: "StlgPaid", excelHeader: "PPnBM yang dikreditkan pada SPT yang Dilaporkan" } - ]; - const excelData = filteredRefundData.map(item => { - const row = {}; - headerMapping.forEach(map => { - if (map.apiField === 'ReportedBySeller') { - row[map.excelHeader] = item[map.apiField] ? 'Yes' : 'No'; - } else { - row[map.excelHeader] = item[map.apiField] === null ? '' : item[map.apiField]; - } - }); - return row; - }); - const worksheet = XLSX.utils.json_to_sheet(excelData); - const workbook = XLSX.utils.book_new(); - XLSX.utils.book_append_sheet(workbook, worksheet, "Refund Review"); - XLSX.writeFile(workbook, filename); + const tabBar = document.getElementById("ct-tab-bar"); + if (tabBar) { + tabBar.addEventListener("click", (e) => { + if (e.target.matches(".ct-tab-button")) switchTab(e.target.dataset.tab); + }); } - function toggleAllGroups(containerSelector, button) { - const isCollapsing = button.textContent === 'Collapse All'; - const headers = document.querySelectorAll(`${containerSelector} .group-header`); - const members = document.querySelectorAll(`${containerSelector} .group-member`); + const casesFilterEl = document.getElementById("cases-status-filter"); + if (casesFilterEl) + casesFilterEl.addEventListener("change", renderMyCasesTable); - headers.forEach(h => { - h.classList.toggle('expanded', !isCollapsing); - h.classList.toggle('collapsed', isCollapsing); - }); - members.forEach(m => { - m.style.display = isCollapsing ? 'none' : 'table-row'; - }); + const docsFilterEl = document.getElementById("docs-status-filter"); + if (docsFilterEl) + docsFilterEl.addEventListener("change", renderCaseDocumentsTable); - button.textContent = isCollapsing ? 'Expand All' : 'Collapse All'; - } + const usersFilterEl = document.getElementById("users-role-filter"); + if (usersFilterEl) + usersFilterEl.addEventListener("change", renderCaseUsersTable); - // --- MAIN INITIALIZATION --- - function main() { - addStyles(); - createSidebar(); - document.getElementById('ct-sidebar-toggle').addEventListener('click', () => { - document.getElementById('ct-sidebar').classList.toggle('open'); - }); - document.getElementById('ct-tab-bar').addEventListener('click', (e) => { - if (e.target.matches('.ct-tab-button')) switchTab(e.target.dataset.tab); - }); - document.getElementById('cases-status-filter').addEventListener('change', renderMyCasesTable); - document.getElementById('docs-status-filter').addEventListener('change', renderCaseDocumentsTable); - document.getElementById('users-role-filter').addEventListener('change', renderCaseUsersTable); - document.getElementById('refund-reported-filter').addEventListener('change', renderRefundReviewTable); - document.getElementById('refund-download-btn').addEventListener('click', downloadRefundExcel); + const refundFilterEl = document.getElementById("refund-reported-filter"); + if (refundFilterEl) + refundFilterEl.addEventListener("change", renderRefundReviewTable); - document.getElementById('toggle-cases-btn').addEventListener('click', (e) => toggleAllGroups('#tab-my-cases .results-container', e.target)); - document.getElementById('toggle-docs-btn').addEventListener('click', (e) => toggleAllGroups('#tab-docs .results-container', e.target)); - document.getElementById('toggle-refund-btn').addEventListener('click', (e) => toggleAllGroups('#tab-refund .results-container', e.target)); + const refundDownloadBtn = document.getElementById("refund-download-btn"); + if (refundDownloadBtn) + refundDownloadBtn.addEventListener("click", downloadRefundExcel); - fetchMyCases(); - } + const toggleCasesBtn = document.getElementById("toggle-cases-btn"); + if (toggleCasesBtn) + toggleCasesBtn.addEventListener("click", (e) => + toggleAllGroups("#tab-my-cases .results-container", e.target), + ); + const toggleDocsBtn = document.getElementById("toggle-docs-btn"); + if (toggleDocsBtn) + toggleDocsBtn.addEventListener("click", (e) => + toggleAllGroups("#tab-docs .results-container", e.target), + ); + + const toggleRefundBtn = document.getElementById("toggle-refund-btn"); + if (toggleRefundBtn) + toggleRefundBtn.addEventListener("click", (e) => + toggleAllGroups("#tab-refund .results-container", e.target), + ); + + // initial load + fetchMyCases(); + } + + // Run main when DOM is ready (script runs at document-idle but ensure body exists) + if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", main); + } else { main(); -})(); \ No newline at end of file + } +})();